# 战斗基-联合类型的分发特性
分发 (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」能成立的关键 —— 它故意利用了"分发再逆变推断"的组合。
# 被谁用到
- 判 never / 判 union / 判 any:medium/1042-isNever, medium/1097-isUnion, hard/223-isAny, medium/4484-isUnion。
- 逐支处理:
Exclude,Extract, easy/43-Exclude, medium/645-Diff, medium/949-AnyOf。 - 联合转交叉:hard/55-UnionToIntersection。
- mapped + 索引访问拉平:medium/2946-ObjectEntries, medium/2595-PickByType。
分发是 TS 类型层面的"基础操作系统",上述所有技巧几乎都建立在它之上。多推几次、背下来"裸泛型 + 联合入参 = 分发;元组包一下 = 关分发"这条规律,后面的题就都能丝滑应对。
← 算法-排列组合大乱炖 联系我 →