巧用 TypeScript--- infer

Posted FENews

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了巧用 TypeScript--- infer相关的知识,希望对你有一定的参考价值。

介绍

infer 最早出现在此 PR 中,表示在 extends 条件语句中待推断的类型变量。

简单示例如下:

 
   
   
 
  1. type ParamType<T> = T extends (param: infer P) => any ? P : T;

在这个条件语句 Textends(param:infer P)=>any?P:T 中, infer P 表示待推断的函数参数。

整句表示为:如果 T 能赋值给 (param:infer P)=>any,则结果是 (param:infer P)=>any 类型中的参数 P,否则返回为 T

 
   
   
 
  1. interface User {

  2. name: string;

  3. age: number;

  4. }


  5. type Func = (user: User) => void


  6. type Param = ParamType<Func>; // Param = User

  7. type AA = ParamType<string>; // string

内置类型

在 2.8 版本中,TypeScript 内置了一些与 infer 有关的映射类型:

用于提取函数类型的返回值类型:

 
   
   
 
  1. type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

相比于文章开始给出的示例, ReturnType<T> 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型。

 
   
   
 
  1. type Func = () => User;

  2. type Test = ReturnType<Func>; // Test = User

用于提取构造函数中参数(实例)类型:

一个构造函数可以使用 new 来实例化,因此它的类型通常表示如下:

 
   
   
 
  1. type Constructor = new (...args: any[]) => any;

infer 用于构造函数类型中,可用于参数位置 new(...args:infer P)=>any; 和返回值位置 new(...args:any[])=>infer P;

因此就内置如下两个映射类型:

 
   
   
 
  1. // 获取参数类型

  2. type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;


  3. // 获取实例类型

  4. type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;


  5. class TestClass {


  6. constructor(

  7. public name: string,

  8. public age
    : number

  9. ) {}

  10. }


  11. type Params = ConstructorParameters<typeof TestClass>; // [string, numbder]


  12. type Instance = InstanceType<typeof TestClass>; // TestClass

一些用例

至此,相信你已经对 infer 已有基本了解,我们来看看一些使用它的「骚操作」:

tuple 转 union,如: [string,number] -> string|number

解答之前,我们需要了解 tuple 类型在一定条件下,是可以赋值给数组类型:

 
   
   
 
  1. type TTuple = [string, number];

  2. type TArray = Array<string | number>;


  3. type Res = TTuple extends TArray ? true : false; // true

  4. type ResO = TArray extends TTuple ? true : false; // false

因此,在配合 infer 时,这很容做到:

 
   
   
 
  1. type ElementOf<T> = T extends Array<infer E> ? E : never


  2. type TTuple = [string, number];


  3. type ToUnion = ElementOf<ATuple>; // string | number

在 stackoverflow 上看到另一种解法,比较简(牛)单(逼):

 
   
   
 
  1. type TTuple = [string, number];

  2. type Res = TTuple[number]; // string | number

union 转 intersection,如: 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 左边部分的联合类型,举个例子:在条件类型 TextendsU?X:Y 中,当 TA|B 时,会拆分成 AextendsU?X:Y|BextendsU?X:Y

有了这个前提,再利用在逆变位置上,同一类型变量的多个候选类型将会被推断为交叉类型的特性,即

 
   
   
 
  1. type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;

  2. type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string

  3. type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number

因此,综合以上几点,我们可以得到在 stackoverflow 上的一个答案:

 
   
   
 
  1. type UnionToIntersection<U> =

  2. (U extends any ? (k: U)=>void : never) extends ((k: infer I)=> void) ? I : never;


  3. type Result = UnionToIntersection<string | number>; // string & number

当传入 string|number 时:

  • 第一步: (Uextendsany?(k:U)=>void:never) 会把 union 拆分成 (stringextendsany?(k:string)=>void:never)|(numberextendsany?(k:number)=>void:never),即是得到 (k:string)=>void|(k:number)=>void

  • 第二步: (k:string)=>void|(k:number)=>voidextends((k:infer I))=>void?I:never,根据上文,可以推断出 I 为 string&number

当然,你可以玩出更多花样,比如 union 转 tuple。

一道 TypeScript 面试题

前段时间,在 GitHub 上,发现一道来自 LeetCode TypeScript 的面试题,比较有意思,题目的大致意思是:

假设有一个这样的类型(原题中给出的是类,这里简化为 interface):

 
   
   
 
  1. interface Module {

  2. count: number;

  3. message: string;

  4. asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;

  5. syncMethod<T, U>(action: Action<T>): Action<U>;

  6. }

在经过 Connect 函数之后,返回值类型为

 
   
   
 
  1. type Result {

  2. asyncMethod<T, U>(input: T): Action<U>;

  3. syncMethod<T, U>(action: T): Action<U>;

  4. }

其中 Action<T> 的定义为:

 
   
   
 
  1. interface Action<T> {

  2. payload?: T

  3. type: string

  4. }

这里主要考察两点

  • 挑选出函数

  • 此篇文章所提及的 infer

挑选函数,在 handlebook 中已经给出方法,只需判断 key 的值是 Function 就行了:

 
   
   
 
  1. type FuncName<T> = {

  2. [P in keyof T]: T[P] extends Function ? P : never;

  3. }[keyof T];


  4. type Connect = (module: Module) => { [T in FuncName<Module>]: Module[T] }

  5. /*

  6. * type Connect = (module: Module) => {

  7. * asyncMethod: <T, U>(input: Promise<T>) => Promise<Action<U>>;

  8. * syncMethod: <T, U>(action: Action<T>) => Action<U>;

  9. * }

  10. */

接下来就比较简单了,主要是利用条件类型 + infer,如果函数可以赋值给 asyncMethod<T,U>(input:Promise<T>):Promise<Action<U>>,则取值为 asyncMethod<T,U>(input:T):Action<U>。具体答案就不给出了,感兴趣的小伙伴可以尝试一下。

更多

  • https://jkchao.github.io/typescript-book-chinese/

  • https://jkchao.cn/article/5c162137e35fb85c4c7e1278

  • https://jkchao.cn/article/5befe57994307c57d4c8d383

  • https://jkchao.cn/article/5bde8fdf94307c57d4c8d37a

  • https://jkchao.cn/article/5bb9c63963a5d23d5ce3091b

参考

  • https://zhuanlan.zhihu.com/p/58704376

  • http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html

  • https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type




以上是关于巧用 TypeScript--- infer的主要内容,如果未能解决你的问题,请参考以下文章

Typescript — infer

Typescript — infer

TypeScript `infer` 关键字

[TypeScript] Generic Functions, class, Type Inference and Generics

奇舞周刊第 266 期: 巧用 Typescript

白话typescript中的extends和infer(含vue3的UnwrapRef)