Kodokon kodokon.com

条件类型与 infer:编译器的逻辑

掌握条件类型的分配性、用 infer 进行模式匹配,以及 never、any 和 boolean 等边缘情况。

10 分钟 · 3 题

在 Kodokon 中打开本课

条件类型是类型系统的分支机制:T extends U ? X : Y 询问编译器 T 是否可赋值给 U,然后选择一个分支。所有标准工具类型,例如 ReturnTypeExcludeAwaited,都建立在它们之上。在这个层次上,问题不再是如何编写它们,而是如何精确预测编译器如何求值它们,尤其是在涉及联合类型和 never 时。

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, not false!
在联合类型上得到一个令人意外的结果:boolean。

关键行为是分配性:当被测试的类型是一个裸类型参数(未被包裹在元组、数组或交叉类型中)时,条件会分别应用于联合类型的每个成员,然后再将结果组合起来。因此 IsString<string | number> 计算得到 true | false,也就是 boolean。要禁用这种分配,把两侧都包进元组:[T] extends [U]。一个可怕的边缘情况:never 是空联合类型;在零个成员上进行分配不会求值任何分支,无论你写的是哪个分支,结果都是 never

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tuple
元组中和了分配,让 never 的判断变得可靠。

infer 关键字引入一个通过模式匹配捕获的类型变量:编译器尝试将被测试类型的结构与模式进行匹配,如果匹配成功就绑定该变量。你可以放置多个同名的 infer 声明:在协变位置(产出的值,例如返回类型)中,候选类型会被合并为联合类型;在逆变位置(函数参数)中,它们会被组合为交叉类型。这条规则直接源于型变:它是每个方向上唯一安全的选择。

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
逆变位置上的两个 infer:交叉类型。

自 TypeScript 4.7 起,infer 接受内联约束:infer N extends number 会过滤捕获的内容,而自 4.8 起编译器甚至会把数字字符串字面量转换为数字字面量。另一个规范上的细微之处:如果一个条件类型的输入仍然包含未解析的泛型参数,它会保持延迟状态;它只在实例化处才被求值,这就解释了为什么有些错误只在调用方一侧才显现出来。

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">;   // never
对 infer 的约束与字面量转换(4.8)。

知识检测

确认你已牢记本课的重点内容。

  1. 类型 Exclude<boolean, true> 是什么?
    • boolean
    • false
    • never
    • true
  2. 对于 type IsNever<T> = T extends never ? true : false;IsNever<never> 是什么?
    • true
    • false
    • never
    • boolean
  3. 两个同名的 infer P 放置在函数参数位置会产生:
    • 候选类型的联合
    • 候选类型的交叉
    • 遇到的第一个候选类型
    • 一个编译错误