使用 infer 封装通用类型工具

在 TS 里面有一个非常非常重要的关键字叫作 infer。当我们在 TS 里面去书写一些类型工具的时候,十有八九都会用到这个 infer。如果不了解这个关键字,是很难写出强大好用的类型工具的。但是这个关键字掌握起来其实很简单,下面我将会用 4 个例子来展示 infer 的使用场景。

🌰 获取函数的返回类型

我们有两个函数,分别是 sumconcatsum 的返回类型是 numberconcat 的返回类型是 any[]

type sum = (a: number, b: number) => number;
type concat = (a: any[], b: any[]) => any[];

我们有两个函数,我希望有个 Return<T> 能拿到这两个函数的返回值的类型,比如 sum 函数返回的类型是 number, 那么 Return<sum> 的结果就是 number。其实 TS 本身就自带了这个类型工具,叫作 RenterType,但是我们还是要自己实现一遍,目的就是为了去理解 infer 的使用。

那么该怎么去写呢?拿到这个问题我们很容易的就会去想,我们应该先去判断拿到的东西是不是一个函数,如果是个函数,那么我们就返回函数的返回类型,否则就返回 never

type Return<T> = T extends Function ? 函数的返回类型 : never

这个时候我们会发现我们没法去写了,因为我们不知道怎么去获取函数的返回类型。我们希望的是利用 TS 的类型推导能够帮我们直接识别出函数返回结果,这时候就需要去利用上面的三目运算,extends 其实就是在进行类型匹配,在匹配的时候会去匹配函数的参数类型和函数的返回类型。所以我们可以去写一个通用的函数表达式(即所有的函数都能匹配)的参数部分:

type Return<T> = T extends (...args: any[]) => *** ? 函数的返回类型 : never

而这个通用的函数表达式的返回类型,就可以使用 infer 关键字

type Return<T> = T extends (...args: any[]) => infer R ? 函数的返回类型 : never

这样写的话其实 TS 的运行逻辑就会是:

把我们传过来的这个类型 T 拿去跟通用的函数表达式去匹配,在匹配的过程中会检查函数表达式的参数类型和返回值类型,检查参数的类型是否满足要求,检查返回的类型有没有满足要求,这个时候 TS 会发现返回类型这边并不是要求,而是需要把返回类型推导的结果放到 R 里,于是 TS 就会把推导的结果赋给 R。那我们最终返回函数的返回类型就容易写了,因为就是 R

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

🌰🌰 获取函数的第一个参数的类型

我们现在的需求变成了想要定义一个 FirstArg 的工具类型,传入一个函数类型,能够获取这个函数的第一个参数的类型。

type sum = (a: number, b: number) => number;
type concat = (a: any[], b: any[]) => any[];

type FirstArg = ?
type fa = FirstArg<sum> // 期望是 number

还是像第一个问题一样,我们写一个三目表达式

type FirstArg<T> = T extends 函数 ? 第一个参数 : never;

因为我们这次要匹配参数了,所以我们需要也一个通用函数,能够满足所有函数的返回类型,参数部分先不管:

type FirstArg<T> = T extends () => any ? 第一个参数 : never;

现在函数的返回类型是通用的了,那么我们希望 TS 在对函数的参数做匹配的时候,把第一个参数返回给我们,所以我们就需要在第一个参数处使用 infer 关键字,后续的参数则不需要关心,所以要写成通用形式:

type FirstArg<T> = T extends (x: infer F, ...args: any[]) => any ? 第一个参数 : never;

这样的话,TS 就会在对函数的参数做匹配的时候,把第一个参数的类型赋给 F,那么我们只需要返回 F 就可以了:

type FirstArg<T> = T extends (x: infer F, ...args: any[]) => any ? F : never;

🌰🌰🌰 获取 Promise 里的泛型

我想获取一个 Promise 类型里泛型的类型:

type PromiseType<T> = ??;
type pt = PromiseType<Promise<string>>; // string

按照上面的思路,我们只需要在 extends 后面写上 Promise 的通用类型,并且在要获取类型的地方使用 infer:

type PromiseType<T> = T extends Promise<infer R> ? R : never;
type pt = PromiseType<Promise<string>>; // string

🌰🌰🌰🌰 获取数组的类型

我们还可以用到数组里面,比如传入一个数组类型,我想获得这个数组每一项的类型是什么:

type ArrayType<T> = ??;
type ItemType1 = ArrayType<[string, number]>; // string | number
type ItemType2 = ArrayType<string[]>; // string

做法也是一样的:

type ArrayType<T> = T extends (infer R)[] ? R : never;
type ItemType1 = ArrayType<[string, number]>; // string | number
type ItemType2 = ArrayType<string[]>; // string

以上就是对 infer 关键字的一个练习,掌握了这个关键字以后我们在做各种类型体操的时候会更加得心应手。