巧用 TypeScript(五)-- infer

三毛丶發表於2019-03-14

介紹

infer 最早出現在此 PR 中,表示在 extends 條件語句中待推斷的型別變數。

簡單示例如下:

type ParamType<T> = T extends (param: infer P) => any ? P : T;
複製程式碼

在這個條件語句 T extends (param: infer P) => any ? P : T 中,infer P 表示待推斷的函式引數。

整句表示為:如果 T 能賦值給 (param: infer P) => any,則結果是 (param: infer P) => any 型別中的引數 P,否則返回為 T

interface User {
  name: string;
  age: number;
}

type Func = (user: User) => void

type Param = ParamType<Func>;   // Param = User
type AA = ParamType<string>;    // string
複製程式碼

內建型別

在 2.8 版本中,TypeScript 內建了一些與 infer 有關的對映型別:

  • 用於提取函式型別的返回值型別:

    type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
    複製程式碼

    相比於文章開始給出的示例,ReturnType<T> 只是將 infer P 從引數位置移動到返回值位置,因此此時 P 即是表示待推斷的返回值型別。

    type Func = () => User;
    type Test = ReturnType<Func>;   // Test = User
    複製程式碼
  • 用於提取建構函式中引數(例項)型別:

    一個建構函式可以使用 new 來例項化,因此它的型別通常表示如下:

    type Constructor = new (...args: any[]) => any;
    複製程式碼

    infer 用於建構函式型別中,可用於引數位置 new (...args: infer P) => any; 和返回值位置 new (...args: any[]) => infer P;

    因此就內建如下兩個對映型別:

    // 獲取引數型別
    type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
    
    // 獲取例項型別
    type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
    
    class TestClass {
    
      constructor(
        public name: string,
        public string: number
      ) {}
    }
    
    type Params = ConstructorParameters<typeof TestClass>;  // [string, numbder]
    
    type Instance = InstanceType<typeof TestClass>;         // TestClass
    複製程式碼

一些用例

至此,相信你已經對 infer 已有基本瞭解,我們來看看一些使用它的「騷操作」:

  • tupleunion ,如:[string, number] -> string | number

    解答之前,我們需要了解 tuple 型別在一定條件下,是可以賦值給陣列型別:

    type TTuple = [string, number];
    type TArray = Array<string | number>;
    
    type Res = TTuple extends TArray ? true : false;    // true
    type ResO = TArray extends TTuple ? true : false;   // false
    複製程式碼

    因此,在配合 infer 時,這很容做到:

    type ElementOf<T> = T extends Array<infer E> ? E : never
    
    type TTuple = [string, number];
    
    type ToUnion = ElementOf<TTuple>; // string | number
    複製程式碼

    stackoverflow 上看到另一種解法,比較簡(牛)單(逼):

    type TTuple = [string, number];
    type Res = TTuple[number];  // string | number
    複製程式碼
  • unionintersection,如:string | number -> string & number

    這個可能要稍微麻煩一點,需要 infer 配合「 Distributive conditional types 」使用。

    相關連結中,我們可以瞭解到「Distributive conditional types」是由「naked type parameter」構成的條件型別。而「naked type parameter」表示沒有被 Wrapped 的型別(如:Array<T>[T]Promise<T> 等都是不是「naked type parameter」)。「Distributive conditional types」主要用於拆分 extends 左邊部分的聯合型別,舉個例子:在條件型別 T extends U ? X : Y 中,當 TA | B 時,會拆分成 A extends U ? X : Y | B extends U ? X : Y

    有了這個前提,再利用在逆變位置上,同一型別變數的多個候選型別將會被推斷為交叉型別的特性,即

    type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
    type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
    type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number
    複製程式碼

    因此,綜合以上幾點,我們可以得到在 stackoverflow 上的一個答案:

    type UnionToIntersection<U> =
      (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
    
    type Result = UnionToIntersection<string | number>; // string & number
    複製程式碼

    當傳入 string | number 時:

    • 第一步:(U extends any ? (k: U) => void : never) 會把 union 拆分成 (string extends any ? (k: string) => void : never) | (number extends any ? (k: number)=> void : never),即是得到 (k: string) => void | (k: number) => void

    • 第二步:(k: string) => void | (k: number) => void extends ((k: infer I)) => void ? I : never,根據上文,可以推斷出 Istring & number

當然,你可以玩出更多花樣,比如 uniontuple

LeetCode 的一道 TypeScript 面試題

前段時間,在 GitHub 上,發現一道來自 LeetCode TypeScript 的面試題,比較有意思,題目的大致意思是:

假設有一個這樣的型別(原題中給出的是類,這裡簡化為 interface):

interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}
複製程式碼

在經過 Connect 函式之後,返回值型別為

type Result {
  asyncMethod<T, U>(input: T): Action<U>;
  syncMethod<T, U>(action: T): Action<U>;
}
複製程式碼

其中 Action<T> 的定義為:

interface Action<T> {
  payload?: T
  type: string
}
複製程式碼

這裡主要考察兩點

  • 挑選出函式
  • 條件型別 + 此篇文章所提及的 infer

挑選函式的方法,已經在 handbook 中已經給出,只需判斷 value 能賦值給 Function 就行了:

type FuncName<T>  = {
  [P in keyof T]: T[P] extends Function ? P : never;
}[keyof T];

type Connect = (module: Module) => { [T in FuncName<Module>]: Module[T] }
/*
 * type Connect = (module: Module) => {
 *   asyncMethod: <T, U>(input: Promise<T>) => Promise<Action<U>>;
 *   syncMethod: <T, U>(action: Action<T>) => Action<U>;
 * }
*/
複製程式碼

接下來就比較簡單了,主要是利用條件型別 + infer,如果函式可以賦值給 asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>,則取值為 asyncMethod<T, U>(input: T): Action<U>。具體答案就不給出了,感興趣的小夥伴可以嘗試一下。

更多

參考

更多文章,請關注我們的公眾號:

微信服務號

相關文章