掌握条件类型的分配性、用 infer 进行模式匹配,以及 never、any 和 boolean 等边缘情况。
在 Kodokon 中打开本课条件类型是类型系统的分支机制:T extends U ? X : Y 询问编译器 T 是否可赋值给 U,然后选择一个分支。所有标准工具类型,例如 ReturnType、Exclude 或 Awaited,都建立在它们之上。在这个层次上,问题不再是如何编写它们,而是如何精确预测编译器如何求值它们,尤其是在涉及联合类型和 never 时。
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;
// true
type B = IsString<string | number>;
// boolean, not false!关键行为是分配性:当被测试的类型是一个裸类型参数(未被包裹在元组、数组或交叉类型中)时,条件会分别应用于联合类型的每个成员,然后再将结果组合起来。因此 IsString<string | number> 计算得到 true | false,也就是 boolean。要禁用这种分配,把两侧都包进元组:[T] extends [U]。一个可怕的边缘情况:never 是空联合类型;在零个成员上进行分配不会求值任何分支,无论你写的是哪个分支,结果都是 never。
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 tupleinfer 关键字引入一个通过模式匹配捕获的类型变量:编译器尝试将被测试类型的结构与模式进行匹配,如果匹配成功就绑定该变量。你可以放置多个同名的 infer 声明:在协变位置(产出的值,例如返回类型)中,候选类型会被合并为联合类型;在逆变位置(函数参数)中,它们会被组合为交叉类型。这条规则直接源于型变:它是每个方向上唯一安全的选择。
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 }自 TypeScript 4.7 起,infer 接受内联约束:infer N extends number 会过滤捕获的内容,而自 4.8 起编译器甚至会把数字字符串字面量转换为数字字面量。另一个规范上的细微之处:如果一个条件类型的输入仍然包含未解析的泛型参数,它会保持延迟状态;它只在实例化处才被求值,这就解释了为什么有些错误只在调用方一侧才显现出来。
type ToNumber<S extends string> =
S extends `${infer N extends number}` ? N : never;
type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">; // neverExclude<boolean, true> 是什么?type IsNever<T> = T extends never ? true : false;,IsNever<never> 是什么?infer P 放置在函数参数位置会产生: