1. 程式人生 > 實用技巧 >TypeScript 2.0 標記聯合型別

TypeScript 2.0 標記聯合型別

使用標記的聯合型別構建付款方式

假設咱們為系統使用者可以選擇的以下支付方式建模

  • Cash (現金)
  • PayPal 與給定的電子郵件地址
  • Credit card 帶有給定卡號和安全碼

對於這些支付方法,咱們可以建立一個 TypeScript 介面

interface Cash {
  kind: "cash";
}

interface PayPal {
  kind: "paypal",
  email: string;
}

interface CreditCard {
  kind: "credit";
  cardNumber: string;
  securityCode: string;
}

注意,除了必需的資訊外,每種型別都有一個kind屬性,即所謂的判別屬性。這裡每種情況都是字串字面量型別。

現在定義一個PaymentMethod型別,它是我們剛才定義的三種類型的並集。通過這種方式,用宣告PaymentMethod每個變數, 必須具有給定的三種組成型別中的一種:

type PaymentMethod = Cash | PayPal | CreditCard;

現在我們的型別已經就緒,來編寫一個函式來接受付款方法並返回一個讀得懂的話語:

function describePaymentMethod(method: PaymentMethod) {
  switch (method.kind) {
    case "cash":
      // Here, method has type Cash
      return "Cash";

    case "paypal":
      // Here, method has type PayPal
      return `PayPal (${method.email})`;

    case "credit":
      // Here, method has type CreditCard
      return `Credit card (${method.cardNumber})`;
  }
}

首先,該函式包含的型別註釋很少,method引數僅包含一個。除此之外,函式基本是純 ES2015程式碼。

在switch語句的每個case中,TypeScript 編譯器將聯合型別縮小到它的一個成員型別。例如,當匹配到"paypal",method引數的型別從PaymentMethod縮小到PayPal。因此,咱們可以訪問email屬性,而不必新增型別斷言。

本質上,編譯器跟蹤程式控制流以縮小標記聯合型別。除了switch語句之外,它還要考慮條件以及賦值和返回的影響。

function describePaymentMethod(method: PaymentMethod) {
  if (method.kind === "cash") {
    // Here, method has type Cash
    return "Cash";
  }

  // Here, method has type PayPal | CreditCard

  if (method.kind === "paypal") {
    // Here, method has type PayPal
    return `PayPal (${method.email})`;
  }

  // Here, method has type CreditCard
  return `Credit card (${method.cardNumber})`;
}

控制流的型別分析使得使用標記聯合型別非常順利。使用最少的 TypeScript語法開銷,咱可以編寫幾乎純js,並且仍然可以從型別檢查和程式碼完成中受益。

使用標記聯合型別構建 Redux 操作

標記聯合型別真正發揮作用的用例是在 TypeScript 應用程式中使用Redux時。 編寫一個事例,其中包括一個模型,兩個actions和一個Todo應用程式的reducer。

以下是一個簡化的Todo型別,它表示單個todo。這裡使用readonly修飾符為了防止屬性被修改。

interface Todo {
  readonly text: string;
  readonly done: boolean;
}

使用者可以新增新的 todos 並切換現有 todos 的完成狀態。根據這些需求,咱們需要兩個Redux操作,如下所示:

interface AddTodo {
  type: "ADD_TODO";
  text: string;
}

interface ToggleTodo {
  type: "TOGGLE_TODO";
  index: number
}

與前面的示例一樣,現在可以將Redux操作構建為應用程式支援的所有操作的聯合

type ReduxAction = AddTodo | ToggleTodo;

在本例中,type屬性充當判別屬性,並遵循Redux中常見的命名模式。現在新增一個與這兩個action一起工作的Reducer:

function todosReducer(
  state: ReadonlyArray<Todo> = [],
  action: ReduxAction
): ReadonlyArray<Todo> {
  switch (action.type) {
    case "ADD_TODO":
      // action has type AddTodo here
      return [...state, { text: action.text, done: false }];

    case "TOGGLE_TODO":
      // action has type ToggleTodo here
      return state.map((todo, index) => {
        if (index !== action.index) {
          return todo;
        }

        return {
          text: todo.text,
          done: !todo.done
        };
      });

    default:
      return state;
  }
}

同樣,只有函式簽名包含型別註釋。程式碼的其餘部分是純 ES2015,而不是特定於 TypeScript。

我們遵循與前面示例相同的邏輯。基於Redux操作的type屬性,我們在不修改現有狀態的情況下計算新狀態。在switch語句的情況下,我們可以訪問特定於每個操作型別的text和index屬性,而不需要任何型別斷言。

never 型別

TypeScript 2.0引入了一個新原始型別never。never型別表示值的型別從不出現。具體而言,never是永不返回函式的返回型別,也是變數在型別保護中永不為true的型別。

這些是never型別的確切特徵,如下所述:

  • never是所有型別的子型別並且可以賦值給所有型別。
  • 沒有型別是never的子型別或能賦值給never(never型別本身除外)。
  • 在函式表示式或箭頭函式沒有返回型別註解時,如果函式沒有return語句,或者只有never型別表示式的return語句,並且如果函式是不可執行到終點的(例如通過控制流分析決定的),則推斷函式的返回型別是never。
  • 在有明確never返回型別註解的函式中,所有return語句(如果有的話)必須有never型別的表示式並且函式的終點必須是不可執行的。

聽得雲裡霧裡的,接下來,用幾個例子來講講never這位大哥。

永不返回的函式

下面是一個永不返回的函式示例:

// Type () => never
const sing = function() {
  while (true) {
    console.log("我就是不返回值,怎麼滴!");
    console.log("我就是不返回值,怎麼滴!");
    console.log("我就是不返回值,怎麼滴!");
    console.log("我就是不返回值,怎麼滴!");
    console.log("我就是不返回值,怎麼滴!");
    console.log("我就是不返回值,怎麼滴!");
  }
}

該函式由一個不包含break或return語句的無限迴圈組成,所以無法跳出迴圈。因此,推斷函式的返回型別是never。

類似地,下面函式的返回型別被推斷為never

// Type (message: string) => never
const failwith = (message: string) => {
  throw new Error(message);
};

TypeScript 推斷出never型別,因為該函式既沒有返回型別註釋,也沒有可到達的端點(由控制流分析決定)。

不可能有該型別的變數

另一種情況是,never型別被推斷為從不為ture。在下面的示例中,我們檢查value引數是否同時是字串和數字,這是不可能的。

function impossibleTypeGuard(value: any) {
  if (
    typeof value === "string" &&
    typeof value === "number"
  ) {
    value; // Type never
  }
}

這個例子顯然是過於作,來看一個更實際的用例。下面的示例展示了 TypeScript 的控制流分析縮小了型別守衛下變數的聯合型別。直觀地說,型別檢查器知道,一旦咱們檢查了value是字串,它就不能是數字,反之亦然

function controlFlowAnalysisWithNever(
  value: string | number
) {
  if (typeof value === "string") {
    value; // Type string
  } else if (typeof value === "number") {
    value; // Type number
  } else {
    value; // Type never
  }
}

注意,在最後一個else分支中,value既不能是字串,也不能是數字。在這種情況下,TypeScript 推斷出never型別,因為咱們已經將value引數註解為型別為string | number,也就是說,除了string或number,value引數不可能有其他型別。

一旦控制流分析排除了string和number作為value型別的候選項,型別檢查器就推斷出never型別,這是惟一剩下的可能性。但是,咱們也就不能對value做任何有用的事情,因為它的型別是never,所以咱們的編輯器工具不會顯示自動顯示提示該值有哪些方法或者屬性可用。

never 和 void 之間的區別

你可能會問,為什麼 TypeScript 已經有一個void型別為啥還需要never型別。雖然這兩者看起來很相似,但它們是兩個不同的概念:

沒有顯式返回值的函式將隱式返回undefined。雖然我們通常會說這樣的函式“不返回任何東西”,但它會返回。在這些情況下,我們通常忽略返回值。這樣的函式在 TypeScript 中被推斷為有一個void返回型別。

具有never返回型別的函式永不返回。它也不返回undefined。該函式沒有正常的完成,這意味著它會丟擲一個錯誤,或者根本不會完成執行。

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

函式宣告的型別推斷

關於函式宣告的返回型別推斷有一個小問題。咱們前面列出的幾條never特徵,你會發現下面這句話:

在函式表示式或箭頭函式沒有返回型別註解時,如果函式沒有return語句,或者只有never型別表示式的return語句,並且如果函式是不可執行到終點的(例如通過控制流分析決定的),則推斷函式的返回型別是never。

它提到了函式表示式和箭頭函式,但沒有提到函式宣告。也就是說,為函式表示式推斷的返回型別可能與為函式宣告推斷的返回型別不同:

// Return type: void
function failwith1(message: string) {
  throw new Error(message);
}

// Return type: never
const failwith2 = function(message: string) {
  throw new Error(message);
};

這種行為的原因是向後相容性,如下所述。如果希望函式宣告的返回型別never,則可以對其進行顯式註釋:

function failwith1(message: string): never {
  throw new Error(message);
}