跳到主要内容

联合类型(Union)与交叉类型(Intersection)

在 TypeScript 里:

  • 联合类型 A | B:表示值“可能是 A,也可能是 B”(二选一 / 多选一)。
  • 交叉类型 A & B:表示值“同时满足 A 和 B”(把约束叠加在一起)。
先记住这句话

| = 备选(要么这个要么那个),& = 叠加(两个都要满足)。

一、联合类型 |

1)基本写法:把“可能的类型”列出来

type Id = string | number;

const a: Id = "u_1";
const b: Id = 1001;

联合类型经常和字面量类型一起出现,用来表达“只能取这些值之一”:

type Theme = "light" | "dark";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

2)为什么联合类型只能直接访问“共有成员”?

看一个最经典的例子:

type Cat = { meow: () => void; name: string };
type Dog = { bark: () => void; name: string };

function play(pet: Cat | Dog) {
pet.name; // ✅ 两者都有
// pet.meow(); // ❌ 不安全:Dog 没有 meow
// pet.bark(); // ❌ 不安全:Cat 没有 bark
}

原因很简单:pet: Cat | Dog 的含义是运行时可能是 Cat,也可能是 Dog,所以你只能安全地使用它们共同拥有的部分。

3)联合类型的“类型缩小”(Narrowing)

联合类型真正好用的地方在于:你可以通过条件判断,把 A | B 缩小成更具体的 A 或 B。

3.1)typeof:处理基本类型(string/number/boolean/undefined/...)

function formatId(id: string | number) {
if (typeof id === "string") {
return id.toUpperCase(); // id: string
}
return id.toFixed(0); // id: number
}

3.2)in:处理对象联合(看“是否存在某个字段”)

type Square = { size: number };
type Circle = { radius: number };

function area(shape: Square | Circle) {
if ("size" in shape) {
return shape.size ** 2; // shape: Square
}
return Math.PI * shape.radius ** 2; // shape: Circle
}

3.3)instanceof:处理 class 实例联合

class ApiError extends Error {
code = 500;
}

function getMessage(err: Error | ApiError) {
if (err instanceof ApiError) {
return `${err.code}: ${err.message}`;
}
return err.message;
}

3.4)自定义类型守卫:x is T

当内置方式不够用时,可以用类型谓词(Type Predicate):

type User = { id: string; name: string };
type Guest = { guestId: string };

function isUser(value: User | Guest): value is User {
return "id" in value;
}

function greet(value: User | Guest) {
if (isUser(value)) {
value.name; // value: User
} else {
value.guestId; // value: Guest
}
}

3.5)可辨识联合(Discriminated Union):最推荐的建模方式

给每个分支加一个判别字段(通常叫 kind/type/tag),TypeScript 能在 switch/if 中自动缩小。

type Loading = { status: "loading" };
type Success = { status: "success"; data: string[] };
type Failure = { status: "failure"; error: string };

type Result = Loading | Success | Failure;

function render(r: Result) {
switch (r.status) {
case "loading":
return "加载中...";
case "success":
return r.data.join(", ");
case "failure":
return r.error;
}
}

3.6)穷尽性检查:用 never 防止漏分支

上面的 switch 如果未来加了新状态(比如 "empty"),你希望编译器提醒你“这里没处理”。可以加一个 never 断言:

function assertNever(x: never): never {
throw new Error("Unexpected object: " + String(x));
}

function renderStrict(r: Result) {
switch (r.status) {
case "loading":
return "加载中...";
case "success":
return r.data.join(", ");
case "failure":
return r.error;
default:
return assertNever(r); // ✅ 如果漏了分支,这里会报错
}
}

4)联合类型与函数:参数用联合 ≠ 函数重载

很多同学会写出这样的代码:

function parse(input: string | number) {
if (typeof input === "string") return JSON.parse(input);
return input;
}

它的问题是:返回值会变成宽泛的联合类型(这里是 any/unknown 的问题先不谈),调用方很难得到“输入是什么就返回什么”的精准类型。

更常见的方案是函数重载(Overload)让调用更精确:

function toArray(input: string): string[];
function toArray(input: number): number[];
function toArray(input: string | number) {
return [input];
}

const a = toArray("x"); // string[]
const b = toArray(1); // number[]
结论

联合参数更适合“同一套处理逻辑、返回也差不多”的场景;需要“输入不同、返回更精确”时,优先考虑重载或可辨识联合。

二、交叉类型 &

1)对象交叉:把多个对象约束“合并到一起”

交叉类型最常见的用途是:把多个对象类型叠加,得到一个“同时拥有它们所有字段”的类型。

type WithId = { id: string };
type WithCreatedAt = { createdAt: string };

type Entity = WithId & WithCreatedAt;
// { id: string; createdAt: string }

这在组合 props、DTO、配置对象时非常常见:

type BaseProps = { className?: string };
type ButtonProps = BaseProps & { onClick: () => void; disabled?: boolean };

2)冲突字段:交叉不等于“随便拼”,冲突会变成 never

如果两个类型对同一个字段给出了互相矛盾的约束,交叉后的结果会变得“不可满足”。

type A = { id: string };
type B = { id: number };

type C = A & B;
// id: string & number => never

这类类型一般是“造不出来”的:

const c: C = {
// @ts-expect-error 不能同时满足 string 和 number
id: 1,
};
小技巧

当你在交叉类型里看到某个字段突然变成 never,通常意味着:你的类型设计有冲突,或者你把不该交叉的东西交叉了。

3)交叉基本类型:常见结论是 never

type X = string & number; // never
type Y = "a" & "b"; // never

直觉理解:没有一个值能“既是 string 又是 number”。

4)交叉函数类型:有点像“把多个重载签名合在一起”

交叉函数类型常用于把多个调用签名合并成一个“可同时满足”的函数类型:

type Fn =
& ((input: string) => string)
& ((input: number) => number);

declare const fn: Fn;

const a = fn("x"); // string
const b = fn(1); // number

这和“对象交叉”不一样,它更像是“多个调用方式都支持”。(实现时通常还是需要你写运行时代码去区分参数。)

5)&interface extends 的关系

很多情况下,它们都能达到“组合类型”的效果:

type A1 = { id: string };
type B1 = { createdAt: string };

type T = A1 & B1;

interface I extends A1, B1 {}

一般经验:

  • 只需要组合对象结构:两者都可以,团队统一风格即可。
  • 需要组合联合/函数等更复杂的类型:& 更通用(interface 只能 extends 对象类型)。

6)进阶:用交叉做“品牌类型”(Branded Type)

有时我们希望把同样是 string 的东西区分开,比如 UserIdOrderId,避免传错参数:

type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function getUser(id: UserId) {}

const uid = "u_1" as UserId;
// getUser("u_1"); // ❌ 不能直接传 string
getUser(uid); // ✅
注意

品牌类型只在“类型层面”生效,运行时还是普通的字符串;通常需要通过工厂函数/校验函数来创建它,尽量少用随意的 as

三、|& 的组合:常见套路与易错点

1)运算优先级:&| 更“紧”

type T = A | B & C; // 等价于 A | (B & C)

不确定就加括号,别赌优先级。

2)交叉会对联合“分配”(可理解为把约束套到每个分支上)

type T = (A | B) & C; // 常等价于 (A & C) | (B & C)

这也是为什么你经常会看到这样的写法:公共部分 + 变体部分

type Common = { requestId: string };

type Ok = { status: "ok"; data: string[] };
type Err = { status: "err"; message: string };

type Response = Common & (Ok | Err);

这样 Response 里每个分支都有 requestId,同时还能用 status 做缩小。

3)keyof (A | B) 为什么像“交集”?

type A = { a: string; common: number };
type B = { b: string; common: number };

type K1 = keyof (A | B); // "common"
type K2 = keyof (A & B); // "a" | "b" | "common"

直觉解释:

  • A | B:你拿到的是“可能是 A 或 B”,只能安全访问两者都拥有的 key,所以 keyof 更像“交集”。
  • A & B:你拿到的是“同时是 A 和 B”,所有 key 都存在,所以 keyof 更像“并集”。

如果你真的想要“联合对象的所有 key 并起来”,常见写法是:

type KeysOfUnion<T> = T extends any ? keyof T : never;
type KU = KeysOfUnion<A | B>; // "a" | "b" | "common"

四、常见错误清单(写代码时对照)

  • A | B 里直接访问 A 的独有字段(忘了做 narrowing)。
  • A & B “硬拼”两个互斥结构,最后得到一堆 never 字段。
  • 忘了括号导致优先级错误(尤其是 A | B & C)。
  • as 逃过类型检查,但运行时数据不满足约束(类型安全被你自己关掉了)。

面试高频问答

1)A | BA & B 的核心区别是什么?

A | B 表示“可能是 A 或 B”;A & B 表示“必须同时满足 A 和 B”。前者是备选,后者是叠加约束

2)为什么 Cat | Dog 不能直接调用 meow()

因为运行时它可能是 Dog,而 Dog 没有 meow();必须先用类型守卫把它缩小成 Cat

3)联合类型怎么做类型缩小?常见手段有哪些?

typeof(基本类型)、in(对象字段)、instanceof(类实例)、自定义类型守卫(x is T)、可辨识联合(kind/status/type + switch)。

4)可辨识联合(Discriminated Union)为什么推荐?

因为它把“分支区分条件”设计进数据结构里,TypeScript 能自动缩小,switch 配合 never 还能做穷尽性检查,分支扩展更安全。

5)string & number 为什么是 never

因为不存在一个值能同时满足 stringnumber 两种约束,交叉后变成不可达类型 never

6)keyof (A | B) 为什么只有公共 key?

因为 A | B 的变量在运行时可能是其中任何一个,你只能保证公共 key 一定存在,所以 keyof (A | B) 更像“交集”。

7)什么时候用联合参数,什么时候用函数重载?

  • 处理逻辑统一、返回类型不需要很精确:用联合参数更简单。
  • 想要“输入不同、返回更精确”的调用体验:用函数重载(或可辨识联合)更合适。