联合类型操作深度解析
联合类型 A 竖线 B 是或,交叉类型 A 和 B 是且,但要驾驭它们得理解分布式条件类型怎么拆联合、函数参数逆变怎么把联合变交叉。 IsUnion、UnionToTuple、UnionToIntersection 等经典体操背后是类型运算思维,面试能写出即证明真正理解类型系统。
一句话概括
联合类型(A | B)是”或”,交叉类型(A & B)是”且”——但要真正驾驭它们,你需要理解分布式条件类型怎么拆解联合、函数参数逆变怎么把联合变成交叉、以及 IsUnion / UnionToTuple 这些经典体操背后的类型运算思维。
核心知识点
1. 分布式条件类型:自动拆解联合的”语法糖”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当泛型 T 是裸类型参数时,T extends U 会把联合拆开逐条判断
type Distribute<T> = T extends string ? `${T}-str` : T;
type A = Distribute<'a' | 1>; // 'a-str' | 1
// 等价于:('a' extends string ? 'a-str' : 'a') | (1 extends string ? '1-str' : 1)
// ⚠️ 停止分布式:用 [] 或 {} 包裹
type NoDist<T> = [T] extends [string] ? 'yes' : 'no';
type B = NoDist<'x' | 'y'>; // 'no' ← 整体匹配,不拆开
// Exclude 和 Extract 就是这么实现的
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type C = MyExclude<'a' | 'b' | 'c', 'b'>; // 'a' | 'c'
2. 联合转交叉:UnionToIntersection
这是面试经典”反转”操作,用函数参数逆变特性实现:
1
2
3
4
5
// 核心理念:((x: A) => void) | ((x: B) => void) 匹配 (x: infer I) => void 时,I = A & B
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type X = UnionToIntersection<{ a: true } | { b: false }>; // { a: true } & { b: false }
分步拆解:
- 分布式将
A|B变为((k:A)=>void) | ((k:B)=>void) - 整个联合去匹配
(k: infer I) => void,因为函数参数是逆变的,TS 推断I = A & B
3. 判断是否联合:IsUnion
1
2
3
4
5
6
7
8
9
10
type IsUnion<T, U = T> =
[T] extends [never] // never 特判
? false
: T extends U // 分布式检查
? [U] extends [T] ? false : true // 每个成员和自己比
: never;
type Test1 = IsUnion<string>; // false
type Test2 = IsUnion<string | number>; // true
type Test3 = IsUnion<never>; // false
原理:T extends U 分布式执行。U = T(原始类型)。对于联合 'a'|'b','a' extends 'a'|'b' → 检查 ['a'|'b'] extends ['a'] → false → 返回 true(有一个分支不是自己)。对于单类型 'a','a' extends 'a' → 检查 ['a'] extends ['a'] → true → 返回 false。
4. 联合转元组:UnionToTuple
这个操作非常”反直觉”——联合类型是无序的,但元组是有序的。需要用函数重载的逆变技巧:
1
2
3
4
5
6
7
8
9
// 精简版——利用函数返回类型的取交集顺序
type LastOf<T> =
UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never;
// 递归弹出最后一个元素
type UnionToTuple<T, L = LastOf<T>> =
[T] extends [never] ? [] : [...UnionToTuple<Exclude<T, L>>, L];
type T = UnionToTuple<3 | 1 | 2>; // [1, 2, 3] ← 顺序不保证,每次编译可能不同!
重要警告:联合类型的顺序在 TS 内部是不确定的,UnionToTuple 的结果顺序不可依赖。这只是一种类型体操练习,生产代码中不推荐依赖它。
5. K in T 和 T[K] 的联合类型行为
1
2
3
4
5
6
7
8
// 索引访问返回联合类型
type Obj = { a: string; b: number; c: boolean };
type Values = Obj[keyof Obj]; // string | number | boolean
type Keys = keyof Obj; // 'a' | 'b' | 'c'
// 映射类型的遍历
type ToArray<T> = { [K in keyof T]: T[K][] };
type Arr = ToArray<Obj>; // { a: string[]; b: number[]; c: boolean[] }
其实你每天都在用
- Redux action 的联合类型——
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT'; amount: number },reducer 中的switch(action.type)配合action的 discriminated union,TypeScript 自动在 case 分支中窄化类型。这是 TypeScript 最常用的联合类型模式之一。 - React 组件的
children类型——React.ReactNode就是一个巨大的联合类型:string | number | boolean | ReactElement | ReactPortal | null | undefined | ...。它囊括了所有可以放在 JSX 标签之间的东西。 Object.keys的类型陷阱——Object.keys({ a: 1, b: 2 })返回string[]而非('a'|'b')[](这是个谨慎的设计选择),但你可以用(keyof typeof obj)[]做更精确的类型标注,这背后就是keyof(联合类型)。- CSS 属性的联合类型——
type TextAlign = 'left' | 'right' | 'center' | 'justify',用联合类型约束合法的 CSS 值,IDE 自动补全 + 编译时报错,比string类型安全一个量级。 - API 状态的 discriminated union——
type State = { status: 'loading' } | { status: 'success'; data: Data } | { status: 'error'; error: Error },这比用status: string + data?可选字段的模式少写 90% 的类型守卫代码。
常见误解(FAQ)
❌ 误区:「
A & B就是同时具有 A 和 B 的所有属性」 真相:对两个对象类型确实是。但当 A 和 B 有同名属性的不同类型时,A & B的属性类型是两者的交叉。{ x: string } & { x: number }的x类型是string & number(即never,不可能同时满足),这个对象几乎没法用。❌ 误区:「联合类型和元组可以无损互转」 真相:不能。联合类型是无序集合(
'a'|'b'等于'b'|'a'),元组是有序序列。即便用UnionToTuple转出了顺序,那个顺序是 TS 内部实现决定的,随版本可能变化,不可依赖。❌ 误区:「
T extends U如果 T 和 U 都在内建类型系统里,结果一定可控」 真相:any extends string的结果是boolean(既是true也是false),因为any在条件类型中是”薛定谔的猫”。never extends string永远走空(分布式短路)。这两个边界情况经常出 bug。❌ 误区:「分布式条件类型会发生在所有泛型参数上」 真相:只有当泛型参数直接出现在
extends左边(裸类型参数)时才分布式。[T] extends [U]或{ x: T } extends { x: U }都不会触发。这也是IsNever<T>需要用[T] extends [never]的关键原因——T extends never在 T=never 时分布式走了空。
一句话总结
联合类型和交叉类型不只是简单的”或”与”且”——它们是 TypeScript 类型计算的基础单元:分布式条件类型用联合做隐式的 map,函数参数逆变让联合变成交叉,keyof 和索引访问让对象和联合互相转换——理解这三层,你就理解了 TypeScript 类型系统的运算内核。