跳到主要内容

联合类型(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 & numbernever

因为没有任何运行时值能同时是 stringnumber

交叉类型本质上在收窄值集合,收窄到空集合时,TypeScript 就用 never 表示“不可能存在的值”。

4.4 什么场景适合用判别联合

  • 请求状态:加载中 / 成功 / 失败;
  • 组件 props 的互斥分支;
  • 后端返回的不同业务结果。

一句话:只要“状态之间互斥,并且每个状态字段不同”,就适合判别联合。


5. 常见追问

5.1 A & (B | C) 怎么理解

可以理解成:

  • 值一定先满足 A
  • 同时还要满足 BC 其中之一。

如果继续展开,会接近 (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 业务建模里最好用的模式之一。