分布式条件类型
下文默认基于 TypeScript 5.x,并假设开启
strict: true。
面试速答(30 秒版 TL;DR)
- 条件类型的基本形式是
T extends U ? X : Y。 - 当
T是联合类型,并且它以“裸类型参数(naked type parameter)”直接出现在extends左边时,TypeScript 会把联合拆开逐个计算,这就叫分布式条件类型。 - 经典例子:
Exclude<T, U>:把联合中的某些成员过滤掉;Extract<T, U>:把联合中满足条件的成员提取出来。
- 如果你不想让它分发,最常用的写法是给两边包一层元组:
[T] extends [U] ? X : Y。
心智模型:先拆联合,再逐个判断,最后再并回来
把它想成一台“类型过滤器”最容易理解:
- 输入一组联合成员;
- 每个成员单独过一遍规则;
- 留下的结果再重新拼成联合。
1. 条件类型的基本形式
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
如果只看到这里,它还只是普通条件判断。
真正容易卡人的地方在于:T 一旦是联合类型,行为就变了。
2. 什么叫“分布式”
type Wrap<T> = T extends any ? { value: T } : never;
type Result = Wrap<string | number>;
Result 不会是:
{ value: string | number }
而是:
{ value: string } | { value: number }
也就是等价于:
type Result =
| Wrap<string>
| Wrap<number>;
这就是“分布”的含义。
3. 为什么 Exclude 和 Extract 能工作
3.1 Exclude
type MyExclude<T, U> = T extends U ? never : T;
type R = MyExclude<"a" | "b" | "c", "a" | "c">; // "b"
展开来看:
type R =
| ("a" extends "a" | "c" ? never : "a")
| ("b" extends "a" | "c" ? never : "b")
| ("c" extends "a" | "c" ? never : "c");
最后只剩 "b"。
3.2 Extract
type MyExtract<T, U> = T extends U ? T : never;
type R = MyExtract<"a" | "b" | "c", "a" | "c">; // "a" | "c"
本质上就是把联合成员一个个拿出来筛选。
4. 什么时候会触发分布
关键条件只有一个:
T必须是裸类型参数,直接写在extends左边。
会分布:
type ToArray<T> = T extends any ? T[] : never;
不会分布:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
对 string | number 来说:
type A = ToArray<string | number>; // string[] | number[]
type B = ToArrayNonDist<string | number>; // (string | number)[]
这道题是分布式条件类型最经典的辨析题。
5. 怎么阻止分布
5.1 包一层元组
type IsStringExactly<T> = [T] extends [string] ? true : false;
type A = IsStringExactly<string>; // true
type B = IsStringExactly<string | number>; // false
这里不再对 string | number 拆开判断,而是把它整体拿去比较。
5.2 为什么包元组就行
因为此时 T 不再是“裸着”出现在 extends 左边。
TypeScript 不会把 [string | number] 拆成 [string] | [number] 再算,所以分布被关闭了。
6. never 为什么经常出现
分布式条件类型和 never 几乎总是一起出现,因为:
never在联合里会自动消失;- 所以它非常适合做“过滤掉这个成员”的信号值。
例如:
type KeepStrings<T> = T extends string ? T : never;
type R = KeepStrings<string | number | boolean>; // string
展开后是:
string | never | never
最终等价于 string。
7. 工程里最常见的两类用法
7.1 做联合过滤
type Event =
| { type: "click"; x: number; y: number }
| { type: "submit"; formId: string }
| { type: "focus"; target: string };
type ClickEvent = Extract<Event, { type: "click" }>;
这类写法非常适合从大型联合类型里提取某一支。
7.2 批量映射联合成员
type ApiResult<T> = T extends any
? { ok: true; data: T } | { ok: false; error: string }
: never;
如果 T 本身是联合,这个规则会对每个成员分别套一遍。
8. 高频题标准答法
8.1 什么是分布式条件类型
标准口径:
- 当条件类型左边是裸类型参数,且传入的是联合类型时,TypeScript 会把联合成员拆开分别计算,再把结果合并成联合。
8.2 为什么 Exclude 能把联合里的成员删掉
因为它利用了分布:
- 每个联合成员都单独判断一次;
- 命中的成员返回
never; never在联合中会被消掉;- 剩下的就是保留结果。
8.3 如何关闭分布
最标准的答法是:
- 把
T包起来,不让它裸着出现在extends左边,例如[T] extends [U]。
9. 常见追问
9.1 T extends any ? ... : ... 为什么总能进真分支
因为除了 never 以外,任意类型都可赋给 any。
这类写法通常不是为了判断真假,而是为了强制触发分布。
9.2 never 传进去会怎样
never 没有联合成员可分发,所以最后结果通常还是 never。
这也是很多复杂泛型最后推成 never 的根源之一。
9.3 和映射类型有什么区别
- 映射类型主要遍历的是对象键;
- 分布式条件类型主要遍历的是联合成员。
两者常常组合使用,但处理对象维度不同。
易错点 / 坑
- 只会背定义,不会展开推导。面试里最稳的办法是把联合成员手动拆开写一遍。
- 不知道“裸类型参数”这个触发条件,结果解释不清为什么有时分布、有时不分布。
- 忽略
never在联合里会消失,导致看不懂Exclude、Extract的实现。 - 本来想得到
(A | B)[],却写成了A[] | B[],根源通常就是误触发了分布。
速记要点(可背诵)
- 条件类型左边是裸类型参数,传入联合时就会分布。
- 分布就是“拆联合成员分别算,再合并结果”。
Exclude、Extract本质都是利用分布加never做过滤。- 不想分布,就把
T包起来,最常见是[T] extends [U]。