联合类型(Union)与交叉类型(Intersection)
下文默认基于 TypeScript 5.x,并假设开启
strict: true。
面试速答(30 秒版 TL;DR)
- 联合类型
A | B表示“值可能是 A,也可能是 B”,所以在缩小类型(narrowing)之前,你只能安全使用 A 和 B 的公共能力。 - 交叉类型
A & B表示“值必须同时满足 A 和 B”,常见用途是组合多个对象约束,不是“二选一”。 - 面试最容易追问的点有三个:
- 为什么联合类型上只能访问公共属性;
- 为什么
string & number会变成never; - 为什么对象交叉有时能合并成功,有时某个字段会被压成
never。
心智模型:联合是“或”,交叉是“且”
1. 联合类型:表达“多种可能”
1.1 最常见写法
let id: string | number;
id = "user-1";
id = 1001;
它的重点不是“把两个类型拼起来”,而是告诉编译器:
- 这个值在运行时有多种可能;
- 你写代码时必须先判断是哪一种,再做对应操作。
1.2 为什么只能访问公共成员
function print(value: string | string[]) {
value.slice(0, 2); // OK,二者都有 slice
// value.toUpperCase(); // Error,因为 string[] 没有 toUpperCase
}
原因很直接:
- 如果编译器允许你直接调
toUpperCase(); - 那么当运行时传进来的是
string[],这段代码就会炸。
所以联合类型的默认策略是保守的,只放行“所有成员都确定有”的能力。
1.3 联合类型真正好用的地方:缩小类型
function formatValue(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
常用缩小手段:
typeof:适合原始类型;instanceof:适合类实例;in:适合对象属性分支;- 判别字段(discriminated union):工程里最稳。
1.4 判别联合是面试高频
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; message: string };
type RequestState = LoadingState | SuccessState | ErrorState;
function render(state: RequestState) {
switch (state.status) {
case "loading":
return "加载中";
case "success":
return state.data.join(", ");
case "error":
return state.message;
}
}
标准答法:
- 联合类型本身不可怕;
- 真正让它可维护的是“每个分支都有稳定的判别字段”;
- 这样编译器才能跟着你的控制流做精确缩小。
2. 交叉类型:表达“同时满足”
2.1 对象交叉最常见
type WithId = { id: string };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type Entity = WithId & WithTimestamps;
const item: Entity = {
id: "42",
createdAt: new Date(),
updatedAt: new Date(),
};
这里的 Entity 不是“可能有 id,也可能有时间戳”,而是这两组字段都要有。
2.2 交叉不是“对象合并函数”
很多人第一次学时会误以为:
A | B是“合并”;A & B是“取交集字段”。
这两个理解都不对。
更准确地说:
- 联合类型是 值集合变大;
- 交叉类型是 约束变严格。
2.3 字段冲突时为什么会出 never
type A = { name: string };
type B = { name: number };
type C = A & B;
C["name"] 会变成 string & number,而这在 TypeScript 里不可能同时成立,所以结果会被压成 never。
这就是交叉类型最常见的追问点:
- 对象层面看像“合并”;
- 但字段层面实际是在做“每个属性都必须同时兼容”。
2.4 原始类型交叉通常没有业务意义
type Impossible = string & number; // never
因为一个值不可能既是 string 又是 number,所以结果只能是 never。
但对象交叉很常见,因为对象可以天然叠加多个约束。
3. 联合与交叉放在一起怎么理解
| 维度 | 联合 A | B | 交叉 A & B |
| --- | --- | --- |
| 语义 | 满足其一即可 | 必须同时满足 |
| 值集合 | 更大 | 更小 |
| 使用感受 | 写分支判断 | 写组合约束 |
| 常见场景 | API 返回多态结果、组件状态机 | 组合对象能力、mixin 风格类型 |
| 典型风险 | 未缩小就乱访问属性 | 冲突字段变 never |
4. 高频题标准答法
4.1 联合类型和交叉类型有什么区别
标准口径:
- 联合类型是“或”,描述一个值可能属于多个类型中的某一个;
- 交叉类型是“且”,描述一个值必须同时满足多个类型约束;
- 联合更像分支建模,交叉更像能力叠加。
4.2 为什么联合类型上只能访问公共属性
因为编译器必须保证这段代码对联合中的每个成员都安全。
如果某个成员没有这个属性或方法,就必须先缩小类型,否则会有运行时风险。
4.3 为什么 string & number 是 never
因为没有任何运行时值能同时是 string 和 number。
交叉类型本质上在收窄值集合,收窄到空集合时,TypeScript 就用 never 表示“不可能存在的值”。
4.4 什么场景适合用判别联合
- 请求状态:加载中 / 成功 / 失败;
- 组件 props 的互斥分支;
- 后端返回的不同业务结果。
一句话:只要“状态之间互斥,并且每个状态字段不同”,就适合判别联合。
5. 常见追问
5.1 A & (B | C) 怎么理解
可以理解成:
- 值一定先满足
A; - 同时还要满足
B或C其中之一。
如果继续展开,会接近 (A & B) | (A & C) 的效果。
5.2 交叉类型和接口 extends 有什么关系
interface X extends A, B本质也是把多个约束组合到一个类型上;A & B更灵活,适合类型别名、泛型计算、临时组合;extends更偏声明式建模。
5.3 为什么联合类型经常配合 never 做穷尽检查
function assertNever(x: never): never {
throw new Error(`unexpected value: ${x}`);
}
如果 switch 漏掉某个联合分支,最后就无法把变量收窄到 never,编译器会提醒你没处理全。
易错点 / 坑
- 把联合类型理解成“字段自动合并”。实际上联合会让你只能访问公共部分。
- 把交叉类型理解成“随便拼起来都能用”。字段一旦冲突,就可能出现
never。 - 对判别联合不用字面量字段,改用松散布尔值或可选字段,后续缩小会很难维护。
- 看到
never只会删类型,不去追源头。很多时候是交叉冲突或条件类型分发导致的。
速记要点(可背诵)
|是“或”,建模多种可能;&是“且”,建模同时满足。- 联合类型默认只能访问公共成员,想用某个分支专属能力必须先缩小。
- 交叉类型常用于对象能力叠加;字段冲突时可能把属性压成
never。 - 判别联合是 TypeScript 业务建模里最好用的模式之一。