【TypeScript 演化史 -- 3】標記聯合類型 與 never 類型

做者:Marius Schulzhtml

譯者:前端小智前端

來源:Marius Schulzgit


阿里雲最近在作活動,低至2折,真心以爲很划算了,能夠點擊本條內容或者連接進行參與promotion.aliyun.com/ntms/yunpar…github

騰訊雲最近在作活動,百款雲產品低至 1 折,能夠點擊本條內容或者連接進行參與typescript


TypeScript 2.0 實現了一個至關有用的功能:標記聯合類型,您可能將其稱爲 sum 類型或與其餘編程語言區別開的聯合類型。 標記聯合類型是其成員類型都定義了字面量類型的區分屬性的聯合類型。編程

上面的講的是理論性的,來幾個例子看看更貼切。安全

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

假設我們爲系統用戶能夠選擇的如下支付方式建模微信

  • 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 語句的狀況下,咱們能夠訪問特定於每一個操做類型的 textindex 屬性,而不須要任何類型斷言。

never 類型

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

這些是 never 類型的確切特徵,以下所述:

  • never 是全部類型的子類型而且能夠賦值給全部類型。
  • 沒有類型是 never 的子類型或能賦值給 nevernever類型自己除外)。
  • 在函數表達式或箭頭函數沒有返回類型註解時,若是函數沒有 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("我就是不返回值,怎麼滴!");
  }
}
複製代碼

該函數由一個不包含 breakreturn 語句的無限循環組成,因此沒法跳出循環。所以,推斷函數的返回類型是 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,也就是說,除了stringnumber, value 參數不可能有其餘類型。

一旦控制流分析排除了 stringnumber 做爲 value 類型的候選項,類型檢查器就推斷出never 類型,這是唯一剩下的可能性。可是,我們也就不能對 value 作任何有用的事情,由於它的類型是 never,因此我們的編輯器工具不會顯示自動顯示提示該值有哪些方法或者屬性可用。

never 和 void 之間的區別

你可能會問,爲何 TypeScript 已經有一個 void 類型爲啥還須要 never 類型。雖然這二者看起來很類似,但它們是兩個不一樣的概念:

沒有顯式返回值的函數將隱式返回 undefined 。雖然咱們一般會說這樣的函數「不返回任何東西」,但它會返回。在這些狀況下,咱們一般忽略返回值。這樣的函數在 TypeScript 中被推斷爲有一個 void 返回類型。

具備 never 返回類型的函數永不返回。它也不返回 undefined。該函數沒有正常的完成,這意味着它會拋出一個錯誤,或者根本不會完成運行。

函數聲明的類型推斷

關於函數聲明的返回類型推斷有一個小問題。我們前面列出的幾條 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);
}
複製代碼

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:

mariusschulz.com/blog/tagged…

mariusschulz.com/blog/the-ne…

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索