qrcode

关注全栈修仙之路,一起学习进阶!

TypeScript infer 关键字

Welcome to the Mastering TypeScript series. This series will introduce the core knowledge and techniques of TypeScript in the form of animations. Let’s learn together! Previous articles are as follows:

阅读须知:本文示例的运行环境是 TypeScript 官网的 Playground,对应的编译器版本是 v3.8.3

一、类型提取

在 TypeScript 中我们能够很方便地从复合类型中提取出单个类型,以数组、元组或对象为例,我们可以通过成员访问的语法来提取数组、元组或对象中元素或属性的类型,具体示例如下:

1
2
3
4
5
6
7
8
9
10
type Person = {
name: string;
age: number;
}

type PersonName = Person["name"]; // string
type StrNumTuple = [string, number];
type StrNumTuple0 = StrNumTuple[0]; // string
type NumArr = number[];
type NumArrMember = NumArr[0]; // number

对象访问语法同样也适用于接口,使用起来非常直观:

1
2
3
4
5
6
interface Person {
name: string;
age: number;
}

type PersonName = Person["name"]; // string

但是,更有趣的是,我们也可以从泛型和函数中提取类型。假设我们有以下的字典类型:

1
2
3
4
5
interface Dictionary<T = any> {
[key: string]: T;
}

type StrDict = Dictionary<string>

为了从 StrDict 类型中提取 T 类型,我们可以使用上面成员属性的方式:

1
type StrDictMember = StrDict[""]; // string

二、条件类型及 infer

其实除了使用以上的方式外,我们还有另一种选择,就是使用 TypeScript 中的 infer 关键字和条件类型:

1
2
type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict>

在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。

条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

1
T extends U ? X : Y

以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。这很好理解,但在 T extends Dictionary<infer V> ? V : never 条件表达式中却多了一个 infer 关键字。在条件类型表达式中,我们可以用 infer 声明一个类型变量并且对它进行使用。

了解完条件类型和 infer 关键字,我们再来看一下完整的代码:

1
2
3
4
5
6
7
8
interface Dictionary<T = any> {
[key: string]: T;
}

type StrDict = Dictionary<string>

type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string

除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function stringPromise() {
return "Hello, Semlinker!";
}

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

async function personPromise() {
return { name: "Semlinker", age: 30 } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person

三、ReturnType

TypeScript 官方类型库中提供了 RetrunType 可获取方法的返回类型,其使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any

// Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T6 = ReturnType<string>; // Error
// Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
type T7 = ReturnType<Function>; // Error

为什么 ReturnType<string>ReturnType<Function> 会抛出上述的异常呢?要解答这个问题,我们就需要来看一下 ReturnType 的定义:

1
2
3
4
5
6
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T
extends (...args: any) => infer R ? R : any;

很明显 ReturnType 内部也是利用条件类型和 infer 关键字,来实现获取方法的返回类型。同理,我们也可以获取参数的类型:

1
2
3
4
type Fn1 = (a: number) => string;
type ArgType<T> = T extends ((a: (infer U)) => any) ? U : never;

type Fn1Arg = ArgType<Fn1>; // number

如果你想要抽取函数中元组类型的所有参数的类型,这就变得更加有趣,在 TypeScript 3.0 版本之后,元组也支持剩余参数与展开参数,因此我们可以通过定义 ArgsType<T> 类型,来实现上述功能,具体代码如下:

1
2
3
4
5
type VariadicFn<A extends any[]> = (...args: A) => any;
type ArgsType<T> = T extends VariadicFn<infer A> ? A : never;

type Fn2 = (a: number, b: string) => string;
type Fn2Args = ArgsType<Fn2>; // [number, string]

infer 关键字除了上述介绍的应用场景之外,它还可以用于实现元组类型转联合类型、联合类型转交叉类型等,这里就不详细展开,大家如果有兴趣的话,可以阅读 深入理解 TypeScript - infer 章节的相关内容。

为了加深大家对 infer 关键字的理解,最后我们再来分析两个相对简单的示例。

示例一:

1
2
3
4
5
type extractArrayType<T> = T extends (infer U)[] ? U : never; 
let stringType : extractArrayType<["test"]> = "test";

// Type '"test"' is not assignable to type 'never'.
let stringTypeNoArray : extractArrayType<"test"> = "test"; // Error

在上面代码中,我们使用泛型语法定义了一个名为 extractArrayType 的条件类型,该条件类型会判断是否类型 T 是属于数组类型,如果满足条件的话,我们使用 infer 关键字来声明一个新的类型变量 U 并返回该类型,否则返回 never 类型。这个例子相对比较简单,我们来看一个相对复杂的例子。

示例二:

1
2
3
4
5
6
7
type InferredAb<T> = T extends { a: infer U, b: infer U } ? U : T; 
type abInferredNumber = InferredAb< { a :number, b: number}>;
let abinf : abInferredNumber = 1;

type abInferredNumberString = InferredAb< { a :number, b: string}>;
let abinfstr : abInferredNumberString = 1;
abinfstr = "test";

在上面代码中,我们使用泛型语法定义了一个名为 InferredAb 的条件类型,该条件类型会判断是否类型 T 是否包含 a 和 b 属性,如果满足条件的话,我们使用 infer 关键字来声明一个新的类型变量 U 并返回该类型,否则返回原有的类型 T。

对于 InferredAb<{ a :number, b: number}> 来说,因为 a 和 b 属性的类型都是 number,所以 abInferredNumber 也是 number 类型。但对于 InferredAb< { a :number, b: string}> 来说,

a 属性的类型是 number,这意味着 a: infer U 将返回 number 类型,而 b 属性的类型是 string,这意味着 b: infer U 将返回 string 类型。因此最终的返回类型将会是联合类型,即 number | string

四、参考资源


欢迎小伙伴们订阅全栈修仙之路,及时阅读 TypeScript、Node/Deno、Angular 技术栈最新文章。

qrcode