Understanding infer in TypeScript

TypeScript 中的 infer 关键字在一般的应用开发时直接使用的情况可能不多,不过在写一些库的时候,就很可能会需要用到了,而且实际上很多本身内置的类型都是通过它来实现的。它有点像我们平时在 js 里使用的解构赋值,只是作用在类型之上的,另外特别要注意的是它只能在条件类型的语句中使用。

条件类型类似 js 里的三元运算符,(condition ? trueExpression : falseExpression)

type T = SomeType extends OtherType ? TrueType : FalseType;

这里的 extends 关键字,声明了一种类型约束,它和 interface A extends B 或者 class A extends B 中的这种表示继承动作的语义很明显是有区别的,但他们确实又有一定的联系,在条件类型中,T extends K 意味着我们是否可以安全的认为 TK 的子类型。

例如,配合 never 类型,我们可以用这种约束关系来缩小类型的范围:

type NullableString = string | null | undefined

type NonNullable<T> = T extends null | undefined ? never : T // Built-in type, FYI

type NonNullableString = NonNullable<NullableString> // evalutes to `string`

或者提取出其中一部分类型:

type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T; // they are also built-in types

type Union = 'a' | 'b' | 'c'
type A = Extract<Union, 'a'>
type BC = Exclude<Union, 'a'>

对于一个数组类型,可以获取到数组的 item 的类型:

// `T[number]` 这里用了一个 `Indexed Access Types` 的技巧,来获取到数组的 item 的类型。
type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

简单说一下什么是 Indexed Access Types 呢,就是直接像访问对象属性一样使用类型,来获取它的某个属性的类型:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number

// 还可以使用 Union
type I1 = Person["age" | "name"]; // number | string

// 或者利用 keyof
type I2 = Person[keyof Person]; // "age" | "name" | "alive"

// 或者其他类型
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName]; // "alive" | "name"

再回到我们的条件类型,先将类型约束到一个具体的类型,然后从中提取出来我们需要部分的类型,这是一个可以借助条件类型来实现的非常常见的场景,但有的时候可能不存在像上面那样能利用 Indexed Access Types 来访问到我们需要的部分的情况,例如获取到函数的参数或者返回类型,为此 ts 提供了一个 infer 关键字,有一点像解构赋值一样,我们在需要的部分前加上 infer 关键字,就能获取到我们需要的部分类型了将它保存起来使用了:

type Flatten<T> = T extends Array<infer Item> ? Item : T;

通过 infer 我们声明了一个新的 Item 的泛型,来获取数组的 Item 的类型,就好像我们通过解构赋值来获取数组的 Item 一样。这里有两个需要注意点的事项:

例如我们可以用来解决提到的获取函数的返回类型,一个函数的类型一般是这样的,(...args: any[]) => any,那么可以有:

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

declare function add(a: number, b: number): number

type A = ReturnType<typeof add>; // number

那么获取函数的参数的类型也很简单了:

type ParamsType<T> = T extends (...args: infer P) => any ? P : any;

type B = ParamsType<typeof add>; // [number, number]

也可以获取函数的第一个参数的类型:

type FirstParamsType<T> = T extends (a: infer P, ...args: any) => any ? P : any;
type C = FirstParamsType<typeof add>; // number

对于多个类型签名的,例如重载过的函数,目前 ts 只支持 infer 使用最后一个类型来进行推断:

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;

type T1 = ReturnType<typeof stringOrNum>; // string | number

当然除了函数,infer 还可以用在任何对象中,比如:

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

type PropertyType<T> =  T extends { id: infer U, name: infer R } ? [U, R] : T
type P = PropertyType<User> // [number, string]

上面我们声明了 U 和 R 两个泛型,最后组合成一个 tuple,那么如果只声明一个泛型,ts 会怎么推断呢?

type PropertyType<T> =  T extends { id: infer U, name: infer U } ? U : T
type P = PropertyType<User> // number | string

在协变位置上,它会返回一个 Union Type。而在逆变位置上,若同一个类型变量存在多个候选者,则最终的类型将被推断为 Intersection Type。

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

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

发现了吗,结合利用函数参数类型的逆变性,我们可以通过 Union Type 来转换成 Intersection Type 了!例如我们希望把很多个对象给拼到一起构成一个类型,可以这样:

// 先转换成一个函数放到参数里,然后求交集
type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer R) => void ? R : never
type A = { a: string }
type B = { b: number }
type T = UnionToIntersection<A | B> // {a: string; b: number}

Update 2022-06-08:

TypeScript 4.7 版本推出了一个新的特性,infer 现在也支持使用 extends constraints 了,例如之前我们需要在使用 infer 之后跟一个嵌套的条件类型:

type FirstIfString<T> =
    T extends [infer S, ...unknown[]]
        ? S extends string ? S : never
        : never;

 // string
type A = FirstIfString<[string, number, number]>;

// never
type D = FirstIfString<[boolean, number, string]>;

而现在,可以直接在使用 infer 的地方使用 extends 来进一步约束类型了:

type FirstIfString<T> =
    T extends [infer S extends string, ...unknown[]]
        ? S
        : never;