TypeScript - Infer关键字

TypeScript的Infer关键字,用于提取类型,创建一个新的类型

·

3 min read

1.背景:

infer关键字的出现,是在这个PR:https://github.com/Microsoft/TypeScript/pull/21496

原文:

This PR introduces the ability to use type inference in conditional types (#21316), enabling pattern matching for types. For example, the following extracts the return type of a function type:

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

它的使用方式,一般是先判断某个泛型是否符合提取的条件,如果满足,则通过infer关键字去进行提取。示例代码,是提取函数参数的情况。

使用的套路一般是这样的:

  • 接收泛型参数(T)

  • 判断泛型参数是否符合某个类型(使用extends),如果符合,提取类型R(使用infer)

type InferWays<T> = T extends infer R ? R : never;

2.使用:

2.1 提取函数的参数类型

思路:我们创建一个FnArgType类型,接收泛型,判断泛型是否符合匹配的类型(使用infer 去提取函数入参类型),如果是,返回infer的类型,否则返回never;

const add = (a:number,b:number) => a+b

type FnArgType<T> = T extends (...args:infer Args) => any ? Args : never;

type FnArgTypeResult = FnArgType<typeof add>
// type FnArgTypeResult = [a: number, b: number]

2.2 提取函数的返回类型

和FnArgType类似,只是infer提取的部分在返参。

const add = (a: number, b: number) => a + b;

type FnReturnType<T> = T extends (...args:any[]) => infer R ? R : never; 

type FnReturnTypeResult = FnReturnType<typeof add>

2.3 提取首尾字符串

要点是将需要判断的字符串类型通过`${infer First}${infer Last}` 的形式提取出来

const str = "abcde";

type FirstLetter<Str extends string> = 
    Str extends `${infer First}${infer Rest}` ? First : never;

type LastLetter<Str extends string> = 
    Str extends `${infer First}${infer Rest}` ? Rest : never;

type FirstLetterTest = FirstLetter<typeof str>; 
// type FirstLetterTest = "a"

type LastLetterTest = LastLetter<typeof str>;
// type LastLetterTest = LastLetter<typeof str>;

2.4 提取字符串类型,拆分成联合类型

示例:

const str = "abc";

type SplitToUnionResult = SplitToUnion<typeof str>
// 等同于 type SplitToUnionResult = "a" | "b" | "c"

实现:

长度不确定,可以通过递归的方式去解决

我们可以接受两个泛型,第一个泛型是需要处理的字符串,第二个泛型是一个累加器

累加器处理的逻辑:当前字符串的类型,和累加器的字符串类型并为联合类型

每次处理一个字符串,将剩余的字符串放入递归中处理

递归结束时,返回累加器处理结果

const str = "abcde";

type SplitToUnion<
  Str extends string,
  Total = never
> = Str extends `${infer First}${infer Rest}`
  ? First extends string
    ? SplitToUnion<Rest, First | Total>
    : never
  : Total;

type R = SplitToUnion<typeof str>;

2.5 提取数组头尾

其实和字符串提取头尾的思路是相似的,对应的格式应该是 [infer First, ...infer Rest]

type FirstItem<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First
  : never;

type LastItem<T extends any[]> = T extends [...infer Rest, infer Last]
  ? Last
  : never;

type Example = ["hello", "world"];

type FirstItemResult = FirstItem<Example>;
// type FirstItemResult = "hello"

type LastItemResult = LastItem<Example>;
// type LastItemResult = "world"

2.6 提取数组头尾+构造

比如我想实现一个ArrayZip类型,接受两个泛型,约束泛型的类型必须是数组

然后将数字的每两项依次按长度进行组合,组合成一个数组,

最终返回一个二维数组的类型

type ArrayZipResult = ArrayZip<[1, 2, 3], [4, 5]>;
// 等同于 type ArrayZipResult = [[1,4],[2,5]]
type ArrayZip<First extends any[], Second extends any[]> = First extends [
  infer FirstHead,
  ...infer FirstRest
]
  ? Second extends [infer SecondHead, ...infer SecondRest]
    ? [[FirstHead, SecondHead], ...ArrayZip<FirstRest, SecondRest>]
    : []
  : [];

// 等同于 type ArrayZipResult = [[1,4],[2,5]]

解决的思路:infer需要提取两个泛型的首个元素,如果均能提取,组合成一个二维数组,并将剩余的元素再使用ArrayZip进行递归操作。

如果无法提取,证明已经处理完毕,返回空数组。

2.6 提取 Promise的返回值

实现一个 PromiseValue类型,用于提取Promise类型的返回类型

type PromiseValue<T> = T extends Promise<infer V> ? V : never;

const service: Promise<number> = new Promise((resolve) => resolve(10));

type PromiseValueTest = PromiseValue<typeof service>;
// type PromiseValueTest = number

严谨一些,我们还可以进行递归处理,比如PromiseValue提取到的如果还是一个Promise类型,再继续做处理

type PromiseValueDeep<T> = T extends Promise<infer U>
  ? U extends Promise<any>
    ? PromiseValueDeep<U>
    : U
  : never;

type PromiseValueDeepResult = PromiseValueDeep<Promise<Promise<Promise<any>>>>;
// type PromiseValueDeepResult = any

2.7 提取构造函数 Constructor 参数

其实和函数提取参数的方式是类似的,唯一区别在于构造函数的类型是:

T extends new (...args: infer Args) => unknown
interface UserInfo {
  name: string;
  age: number;
}

class User {
  userInfo: UserInfo;
  constructor(userInfo: UserInfo) {
    this.userInfo = userInfo;
  }
  get() {
    return this.userInfo;
  }
}

type ConstructorArgs<T> = T extends new (...args: infer Args) => unknown
  ? Args
  : never;

type ConstructorArgsTest = ConstructorArgs<typeof User>;

3.infer 的使用总结

infer的使用:

需要通过 extends 做类型条件判断,通过infer去提取,提取之后可以用于构造新的类型

如果需要构造的新类型长度不确定,可以通过递归+累加的方式去做实现