在 JavaScript 原生语法中,当我们想要根据一个条件来返回不同的值时,通常会使用三元运算符:
条件 ? 真值 : 假值TypeScript 十分敏锐地将这种思维引入了 “类型” 的世界。在处理高级类型体操时,我们经常会遇到这样的需求:“如果传入的泛型是字符串,我就返回类型 A;如果是数字,我就返回类型 B”。
为了实现这种动态的类型分支推导,TypeScript 提供了进阶阶段必备的一个核心功能:条件类型(Conditional Types)。
TypeScript 条件类型简介
TypeScript 中的条件类型的语法,与 JavaScript 的三元运算符是一模一样的,只不过它操作的对象从 “值” 变成了 “类型”。
语法:
T extends U ? X : Y说明:
这里的 extends 已经不再是 “继承” 或者简单的 “泛型约束” 了,它在这里的意思是“兼容性判断”。
整句语法的意思是:如果类型 T 能够赋值给类型 U(即 T 兼容 U),那么返回类型 X,否则返回类型 Y。
示例 1:极简的类型三元运算
// 定义条件类型
type IsString<T> = T extends string ? true : false;
// 传入数字
const res1: IsString<number> = false;
console.log(res1);
// 传入字符串
const res2: IsString<string> = true;
console.log(res2);
// 传入字符串字面量
const res3: IsString<"Jack"> = true;
console.log(res3);运行结果如下。
false
true
true分析:
在这个例子中,我们定义了一个条件类型 IsString:如果传入的 T 是 string,就返回 true,否则返回 false。
TypeScript 条件类型嵌套
在 TypeScript 中,条件类型本质上就是三元运算符,因此它也同样支持 “嵌套”。通过使用条件类型嵌套,我们可以写出复杂的 if-else if-else 逻辑。
示例 2:动态获取类型名称
// 嵌套的条件类型
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object"; // 兜底类型
// 测试各种类型的推导
const t1: TypeName<string> = "string";
const t2: TypeName<1024> = "number"; // 1024 兼容 number
const t3: TypeName<true> = "boolean";
const t4: TypeName<() => void> = "function";
const t5: TypeName<string[]> = "object";
console.log("t2 的类型名是:", t2);
console.log("t4 的类型名是:", t4);运行结果如下。
t2 的类型名是:number
t4 的类型名是:function分析:
在这个例子中,我们定义了一个高级的工具类型 TypeName<T>,它可以根据传入的泛型,返回对应基础类型的字符串字面量。
TypeScript 分布式条件类型
前面介绍的条件类型都非常符合直觉。但是,当条件类型遇到联合类型时,TypeScript 底层会触发一个非常硬核、且经常在大厂面试中被作为压轴题的机制:分布式条件类型(Distributive Conditional Types)。
什么是分布式呢?简单来说,如果 T 是一个泛型,并且你给 T 传入了一个联合类型(比如 A | B),那么 T extends U ? X : Y 不会把 A | B 作为一个整体去和 U 比较。
相反,TypeScript 会把联合类型拆开,然后分别去和 U 比较,最后再把所有的结果联合起来。
(A | B) extends U ? X : Y上面这句代码,其实等价于:
(A extends U ? X : Y) | (B extends U ? X : Y)示例 3:分布式的运行效果
type ToArray<T> = T extends any ? T[] : never;
// 传入联合类型:string | number
type MyArray = ToArray<string | number>;
const arr1: MyArray = ["Jack", "Lucy"]; // string[] 成立
const arr2: MyArray = [100, 200]; // number[] 成立
console.log(arr1);
console.log(arr2);运行结果如下。
[ 'Jack', 'Lucy' ]
[ 100, 200 ]分析:
在这个例子中,我们给 ToArray 传入一个联合类型 “string | number”,此时 MyArray 并不是 (string | number)[],而是被分布计算成了:string[] | number[]。因此,下面这种写法是错误的:
// 报错:不能将类型 “(string | number)[]” 分配给类型 “string[] | number[]”
const arr3: MyArray = ["Jack", 200]; 在前面 “TypeScript 内置泛型工具类型” 一节中我们学习了 Omit<T, K>(从对象中剔除属性),而在处理纯粹的联合类型时,TypeScript 官方提供了另一个专门用来剔除类型的内置工具:Exclude<T, U>。
Exclude<T, U> 的作用是:从联合类型 T 中,剔除所有能赋值给 U 的类型。现在,掌握了 “分布式条件类型”,我们完全可以自己手写一个 Exclude!
示例 4:手写 Exclude 类型过滤器
// 使用分布式条件类型进行过滤
// 遍历 T 中的每一个类型,如果兼容 U,就返回 never(丢弃),否则返回原类型 T
type MyExclude<T, U> = T extends U ? never : T;
// 联合类型 T:"a" | "b" | "c"
// 剔除目标 U:"a" | "c"
type Result = MyExclude<"a" | "b" | "c", "a" | "c">;
const finalRes: Result = "b";
console.log("剔除后的结果:", finalRes);运行结果如下。
剔除后的结果:b分析:
在 TypeScript 中,never 类型代表 “绝对不可能存在的值”,它在联合类型中相当于一个 “空集”。当 never 与其他类型联合时(如 never | "b"),never 会被直接忽略掉。
借助这个特性,我们就可以使用 never 完美实现类型的过滤。
如何阻止分布式条件类型?
虽然分布式条件类型非常强大(比如我们刚刚使用它写出了 Exclude),但有时候我们并不希望 TypeScript 自动拆开联合类型。我们只想把它当做一个 “整体” 来对待。
要阻止分布式特性的触发,语法非常简单:我们只需要在条件类型的泛型两边,加上方括号 “[]” 即可。
示例 5:阻止分布式特性的触发
// 1. 触发分布式的写法(默认)
type ToArray<T> = T extends any ? T[] : never;
// 结果是被拆开的:string[] | number[]
type MyArray1 = ToArray<string | number>;
// 2. 阻止分布式的写法(加方括号)
// 加上方括号后,T 就被死死地绑在了一起,当成了一个整体
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
// 结果是作为一个整体的数组:(string | number)[]
type MyArray2 = ToArrayNonDistributive<string | number>;
// 测试:arr 必须同时能装下字符串和数字
const arr: MyArray2 = ["Jack", 200, "Lucy"];
console.log(arr);运行结果如下。
[ 'Jack', 200, 'Lucy' ]分析:
在 ToArrayNonDistributive 中,我们将 T 和 any 都包裹在了方括号中 [T] extends [any]。这就相当于告诉 TypeScript 编译器:“请不要把联合类型拆开,把它们打包成一个元组来看待。”
通过这种方式,联合类型 string | number 就作为一个不可分割的整体,直接放入了返回结果 T[] 中,最终生成了 (string | number)[]。
