Menu

TypeScript 条件类型与 infer 详解(附示例)

条件类型根据一个判断在两个类型中选出一个:T extends U ? X : Y。本文讲解它的语法、条件类型如何在联合类型上分配(以及如何阻止分配)、用 infer 提取类型,以及如何自己实现 ReturnType。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

条件类型通过一个判断在两个类型之间做选择,写法和 JavaScript 的三元运算符很像:T extends U ? X : Y。如果 T 可以赋值给 U,结果就是 X;否则就是 Y。

这里的 extends 表示“可以赋值给”,和你把值赋给变量时编译器使用的关系相同。条件类型只存在于编译期;它们会从 JavaScript 输出中被擦除。

语法

type Result = CheckedType extends TestType ? TrueType : FalseType;

条件类型和泛型配合才真正有用:被检查的类型是一个类型参数,稍后才会得到具体类型。在 true 分支中,TypeScript 知道被检查的类型符合判断条件,所以即使普通的 T 没有 message 属性,上面的 T["message"] 也是允许的。

条件类型还可以嵌套,就像链式三元表达式一样:

函数体中的 as TypeName<T> 并不是可有可无的,下一节会说明原因。

条件返回类型需要断言

如果函数的返回类型是基于它自身类型参数的条件类型,函数就无法直接返回任何一个分支。TypeScript 不会在函数体内收窄 T,所以它无法判断该用哪个分支:

index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.

两种常见的修复方法:一是重载,写明每一组输入与输出的对应关系,并精确检查调用方;二是在实现中使用断言。

重载在函数重载页面中有详细介绍。使用断言时,编译器会信任你:函数体中返回了错误的分支也不会被发现。

分配式条件类型

当被检查的类型是裸类型参数并且接收到一个联合类型时,条件会对每个成员各运行一次,结果再合并成一个新的联合类型:

Exclude 和 Extract 正是靠分配才能工作。Exclude<T, U> 的定义是 T extends U ? never : T:每个匹配 U 的成员都变成 never,而 never 会从联合类型中消失。所以 Exclude<"a" | "b" | "c", "a"> 的结果是 "b" | "c"。

同一条规则也会带来两个意外。boolean 是联合类型 true | false,所以 ToArray<boolean> 是 false[] | true[],而不是 boolean[]。另外,never 是空的联合类型,所以分配式条件类型收到 never 时,什么都不判断就直接返回 never:

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true

用 infer 提取类型

infer 在 extends 子句中声明一个新的类型变量。如果匹配成功,TypeScript 会根据被检查的类型把这个变量填上,你就可以在 true 分支中使用它:

可以把 T extends Promise<infer V> ? V : T 读作:“如果 T 是某个东西的 promise,就把那个东西叫作 V 并返回它;否则原样返回 T”。infer 只能用在条件类型的 extends 子句中。

infer 变量可以用 extends 带上自己的约束。这样只有推断出的类型满足约束时,匹配才会成功:

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

type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string

自己实现 ReturnType

内置的 ReturnType 就是一行带 infer 的条件类型。自己动手写一遍,是让这两个概念豁然开朗的经典练习:

typeof makeUser 把函数值转换成它的类型,然后条件类型把它与“任意函数”进行匹配,并把返回类型捕获为 R。标准库的版本在两个细节上有所不同:它的参数被约束为函数类型(T extends (...args: any) => any),所以 ReturnType<string> 会报编译错误而不是得到 never;而且它的 false 分支是 any。ReturnType 页面还介绍了 Parameters、InstanceType 和 Awaited。

递归条件类型

条件类型可以引用自身,从而解开任意深度的嵌套:

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

内置的 Awaited<T> 就是这样工作的,它会把 Promise<Promise<T>> 一层层解开,直到得到 T。实际使用时要让递归保持浅层:过深或没有边界的递归会让编译器放弃,并报出 error TS2589: Type instantiation is excessively deep and possibly infinite。

速查表

模式含义
T extends U ? X : Y如果 T 可以赋值给 U 就是 X,否则是 Y
T extends U ? never : T去掉匹配 U 的成员(即 Exclude)
T extends U ? T : never保留匹配 U 的成员(即 Extract)
[T] extends [U] ? X : Y同样的判断,但不在联合类型上分配
T extends (infer E)[] ? E : T数组的元素类型
T extends Promise<infer V> ? V : Tpromise 的值类型
T extends (...args: any[]) => infer R ? R : never函数的返回类型
T extends [infer H, ...infer Rest] ? ...元组的第一个元素和其余部分

常见问题

TypeScript 中的条件类型是什么?

形如 T extends U ? X : Y 的类型。如果 T 可以赋值给 U,结果就是 X,否则是 Y。它是类型层面的 if/else,在编译期求值;生成的 JavaScript 中不会留下它的任何痕迹。

TypeScript 中的 infer 关键字有什么作用?

infer 在条件类型的 extends 子句中声明一个类型变量,并让 TypeScript 根据匹配到的类型把它填上。T extends Promise<infer V> ? V : T 能从 promise 类型中取出它解析后的值类型。它只能用在条件类型的 extends 子句中。

什么是分配式条件类型?

当被检查的类型是一个裸类型参数,而你传入的是联合类型时,条件会分别作用于每个成员,再把结果合并起来。对于 type ToArray<T> = T extends unknown ? T[] : never,ToArray<string | number> 会变成 string[] | number[]。把两边都用方括号包起来,写成 [T] extends [unknown],就能关闭这种行为。

TypeScript 中如何获取函数的返回类型?

使用内置的 ReturnType<typeof fn>。它是一个带 infer 的条件类型:T extends (...args: any) => infer R ? R : any。对于异步函数,再用 Awaited<...> 包一层,就能得到解析后的值而不是 promise。

为什么 IsNever<never> 返回 never 而不是 true?

never 是空的联合类型,而分配式条件类型会对联合类型的每个成员逐一映射。没有成员,就没有东西可映射,结果就是 never。要检测 never 本身,请写成 [T] extends [never] ? true : false。

Coddy 编程语言插图

和 Coddy 一起学编程

开始