# 战斗基-联合类型的分发特性

分发 (distribution) 是 TS 里所有"对联合类型逐支处理"的基石。入门阶段不理解它,就会把 never / any / 联合的行为看成"玄学";理解之后,很多 medium/hard 题的解法都能一眼看穿。

# 定义与最小示例

官方表述来自 Distributive Conditional Types (opens new window)当条件类型 T extends U ? X : Y 里的 T 是一个"裸"的泛型参数,且传入的是联合类型时,TS 会把联合的每一支分别代入做判断,最后把结果拼回联合。

最小示例:

type Case1 = 'x' extends 'x' ? 1 : 2; // 1,没有泛型,不分发
type Case2 = 'x' | 'y' extends 'x' ? 1 : 2; // 2,没有泛型,整体判断

type IsX<T> = T extends 'x' ? 1 : 2;
type Case3 = IsX<'x' | 'y'>; // 1 | 2,分发

关键词是"裸 (naked) 类型参数",意思是 T 必须直接出现在 extends 左侧,不带任何包裹。下一节讲包裹后的行为。

# 关掉分发:[T] extends [U]

想让 TS 把联合当一个整体来判断,就给两边都套一层元组:

type IsXStrict<T> = [T] extends ['x'] ? 1 : 2;

type R = IsXStrict<'x' | 'y'>; // 2

原理:一旦 T 不再"裸"在 extends 左边,分发特性就关闭。元组是最轻量的包裹手段,其它如 (T & {}) 也能达到同样效果。

# 为什么要关分发

# 场景 1:判断 never

never 是 TS 里的"空联合",分发时相当于"零次判断",直接返回 never

type IsNever<T> = T extends never ? true : false;

type A = IsNever<never>; // never,不是 false!

想正确判断 never,必须关分发:

type IsNever<T> = [T] extends [never] ? true : false;

type A = IsNever<never>; // true

这就是 medium/1042-isNever 的核心。

# 场景 2:判断两个类型相等

A extends B 本身会分发,当 A 是联合时会逐支判断,容易出错:

type Eq<A, B> = A extends B ? (B extends A ? true : false) : false;

type R = Eq<'a' | 'b', 'a' | 'b'>; // boolean!

关掉分发就好了:

type Eq<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;

细节见 判断两个类型相等

# 场景 3:判断是否是联合

利用分发的性质反向利用:

type IsUnion<T, U = T> = T extends any
  ? [U] extends [T]
    ? false
    : true
  : never;

type A = IsUnion<'a' | 'b'>; // true
type B = IsUnion<'a'>; // false

这里 T extends any 触发分发,在每一支里保存原始整体 U,然后判断"整体是否还能塞回单支"。见 medium/1097-isUnion

# 利用分发:把联合"批处理"

很多题目需要对联合的每一支单独施加一次相同操作,这时分发就是天然的"for 循环":

// 给联合的每一支包一层
type Wrap<T> = T extends any ? [T] : never;
type R = Wrap<1 | 2 | 3>; // [1] | [2] | [3]

// 过滤联合中不满足条件的支(Exclude 的实现)
type MyExclude<T, U> = T extends U ? never : T;
type R2 = MyExclude<1 | 2 | 3, 2>; // 1 | 3

T extends any ? ... : never 是最常见的"主动触发分发"模板,业内俗称"分发开关"。

# mapped type / 索引访问里的分发行为

除了条件类型,以下两处也会表现出"对联合逐支处理"的效果:

# mapped type 的 key 联合

type O = { a: 1; b: 2 };
type M = { [P in keyof O]: P };
// 等价于对联合 'a' | 'b' 逐支构造,最后合成对象 { a: 'a'; b: 'b' }

这里 keyof O'a' | 'b',mapped type 在键位置上自带"逐支遍历"的能力,效果上和分发一致。

# 索引访问用联合 key

type O = { a: 1; b: 2; c: 3 };
type R = O['a' | 'b']; // 1 | 2

key 是联合时,TS 也会对每一支分别取值再并起来。

利用这两条性质的经典模式:先把每个字段映射成想要的值,再用 [keyof T] 一次性拉成联合:

// 把 value 为 boolean 的字段过滤掉,只留下其它字段的 value
type FilterBoolean<T> = {
  [P in keyof T]: T[P] extends boolean ? never : T[P];
}[keyof T];

这种"mapped + 索引访问"式的分发,是 medium 以上很多对象类题的通用范式,心里要有这个印象。

# 陷阱与边界

# never 在分发里会消失

type F<T> = T extends any ? [T] : never;
type R = F<never>; // never

分发时把 never 当"空集",没有任何一支可以取出,自然没有结果。如果想对 never 也返回某个值,要么关分发,要么在函数入口先判断一次。

# any 本身不是联合

type F<T> = T extends string ? 1 : 2;
type R = F<any>; // 1 | 2

any 不是联合,但它的分发行为非常"薛定谔":既满足也不满足 extends,返回 true | false。这也是判断类型相等时 any 老是惹麻烦的根源。

# 分发只对"直接裸"生效

type F1<T> = T extends string ? T : never; // 分发
type F2<T> = [T] extends [string] ? T : never; // 不分发
type F3<T> = T & {} extends string ? T : never; // 不分发

实际写题时,需要分发就让 T 裸着,不需要就包一下。这也是 hard 技巧「UnionToIntersection」能成立的关键 —— 它故意利用了"分发再逆变推断"的组合。

# 被谁用到

分发是 TS 类型层面的"基础操作系统",上述所有技巧几乎都建立在它之上。多推几次、背下来"裸泛型 + 联合入参 = 分发;元组包一下 = 关分发"这条规律,后面的题就都能丝滑应对。

Last Updated: 2026/4/19 00:27:20