条件类型通过一个判断在两个类型之间做选择,写法和 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 : T | promise 的值类型 |
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。