跳到主要内容

分布式条件类型

下文默认基于 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. 为什么 ExcludeExtract 能工作

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 在联合里会消失,导致看不懂 ExcludeExtract 的实现。
  • 本来想得到 (A | B)[],却写成了 A[] | B[],根源通常就是误触发了分布。

速记要点(可背诵)

  • 条件类型左边是裸类型参数,传入联合时就会分布。
  • 分布就是“拆联合成员分别算,再合并结果”。
  • ExcludeExtract 本质都是利用分布加 never 做过滤。
  • 不想分布,就把 T 包起来,最常见是 [T] extends [U]