Typescript 夜點心:條件範型

今天的夜點心來談一談怎麼寫 TS 中的條件範型git

問題

函數式夜點心:Monad 中咱們提到過 Promisethen 方法是一種同時承載了 mapchain 功能的方法github

  • map 功能:當接收的函數的返回值的類型不是 Promise 時,返回包含了該值的 Promise
  • chain 功能:當接受的函數返回值的類型是 Promise 時,直接返回該值

如今假設咱們須要實現一個名爲 promisify 函數,它的功能有些相似 then 方法:接受一個值,若是它已是 Promise 了,就直接返回;若是不是,就把它包在一個 Promise 中返回。這個需求實現起來不難:typescript

function promisify<T> (input: T) {
  if (input instanceOf Promise) {
    return input;
  }
  return Promise.resolve(input);
}
複製代碼

上面的實現經過範型 T,聲明瞭入參 input 的類型。而Typescript 經過本身的類型推導,只能得出 promisify 的返回類型是 (T & Promise<any>) | Promise<T> ,並不能像咱們的函數邏輯那樣,知道應該在 T 是 Promise 的時候返回 T 類型,不然返回 Promise<T> 類型。這樣類型的定義與函數的邏輯沒有匹配上,會使得 promisify 函數變得很難用:咱們不得不每次都手動斷言它的返回類型,繁瑣且易錯。數組

爲了讓 Typescript 可以根據入參的類型自動判斷出出參的類型,咱們須要用到條件範型:promise

條件範型

條件範型經過相似三元表達式的 extends ? : 的結構和 infer 關鍵字,幫助咱們寫出可以根據條件成立與否進行類型轉換的工具範型。函數

T extends U ? A : B 的結構判斷一個類型 T 是不是類型 U 的子類型,是則返回 A,不是返回 B,例如:工具

type Condition<T> = T extends { name: string } ? string : number;

type Test1 = Condition<{ name: string; value: number }>; // string
type Test2 = Condition<{ value: number }>; // number;
複製代碼

經過條件範型的三元表達式結構,咱們很快就能解決上面提到的問題:post

function promisify<T> (input: T): T extends Promise ? T : Promise<T> {
  // 函數的具體實現
}
複製代碼

infer 關鍵字

如今假設咱們的需求變得更復雜了一點,須要把 Promise 中的值包在一個 { value: T } 的結構中返回,像下面實現的這樣,那麼它的返回類型又應該怎麼聲明呢?優化

function promisify2<T> (input: T) {
  if (input instanceof Promise) {
    return input.then(value => ({ value }));
  }
  return Promise.resolve({ value: input });
}
複製代碼

這時僅僅使用 extends ? : 的結構就不夠了。由於當 TPromise 的子類型時,咱們須要從 T 中「抽取」出它的範型 U 並把它從新包裝起來返回出來。即假設 T = Promise<U>,咱們須要得到 U。這時就須要用到 infer 關鍵字,infer 意爲推斷,能夠用來解構提取咱們須要的類型,用法很簡單:ui

  • extends? 之間使用 infer
  • infer {類型變量名} 放置在須要提取的類型的位置
  • ?: 之間使用推斷獲得的類型變量來構造須要返回的類型

以下的 Unpromise 條件範型就在 T 是 Promise 時經過 infer 關鍵字提取了 Promise 的範型

type Unpromise<T> = T extends Promise<infer U> ? U : T;

type Test1 = Unpromise<number>; // number
type Test2 = Unpromise<Promise<string>>; // string
複製代碼

經過 infer 咱們就可以完成上面 promisify2 函數的返回類型的聲明:

function promisify2<T>( input: T ): T extends Promise<infer U> ? Promise<{ value: U }> : Promise<{ value: T }>
{
  // 具體的實現
};
複製代碼

上述的類型聲明由於邏輯的複雜化變得冗長而難讀,咱們能夠經過適當的類型拆解來優化它的可讀性,同時也能提升類型的複用性:

type Container<T> = Promise<{ value: T }>;
type Unpromise<T> = T extends Promise<U> ? U : T;

function promisify2<T>(input: T): Container<Unpromise<T>> {
  // 具體的實現
}
複製代碼

更多例子:

// 提取數組項的類型
type Unarray<T> = T extends (infer U)[] ? U : never;

// 提取函數的返回值類型(TS 已內置)
type ReturnType<T> = T extends ((...params: any[]) => infer U) ? U : never;

// 提取函數的入參類型(TS 已內置)
type Parameters<T> = T extends ((...params: P) => infer P) ? P : never;

// 元組第一項的類型,可用在 Hooks 風格的 React 組件中
type Head<T> = T extends [infer H, ...any[]] ? H : never;
複製代碼

上面這些例子中的 never 至關於不符合條件則不返回任何類型。

以上就是「條件範型」的相關內容。條件範型使得範型具備函數通常的靈活性,方便咱們定義出與實現邏輯更爲匹配的類型,寫出更優雅強大的 TS 代碼。

github 原文連接

擴展閱讀

相關文章
相關標籤/搜索