TypeScript 条件类型

在 JavaScript 原生语法中,当我们想要根据一个条件来返回不同的值时,通常会使用三元运算符:

条件 ? 真值 : 假值

TypeScript 十分敏锐地将这种思维引入了 “类型” 的世界。在处理高级类型体操时,我们经常会遇到这样的需求:“如果传入的泛型是字符串,我就返回类型 A;如果是数字,我就返回类型 B”。

为了实现这种动态的类型分支推导,TypeScript 提供了进阶阶段必备的一个核心功能:条件类型(Conditional Types)

TypeScript 条件类型简介

TypeScript 中的条件类型的语法,与 JavaScript 的三元运算符是一模一样的,只不过它操作的对象从 “值” 变成了 “类型”。

语法:

T extends U ? X : Y

说明:

这里的 extends 已经不再是 “继承” 或者简单的 “泛型约束” 了,它在这里的意思是“兼容性判断”。

整句语法的意思是:如果类型 T 能够赋值给类型 U(即 T 兼容 U),那么返回类型 X,否则返回类型 Y。

提示: extends 关键字除了可以用于条件类型之外,还可以用于:接口继承类继承泛型约束等。小伙伴们应该多多对比理解一下。

示例 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)[]。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号