这节处在"核心原理"中段,给你描述复杂契约的两个基础算符:联合 A | B 表示"或",交叉 A & B 表示"且"。它们是所有高级类型(第五章)的积木,吃透语义才不会在嵌套时算错。
联合描述"这个值可能是几种类型之一"。对联合变量,只能调用所有成员共有 的方法,除非先收窄(2.2)。
type Id = string | number; function formatId(id: Id): string { // 不能直接 id.toUpperCase(),因为 number 没有 return typeof id === "string" ? id.toUpperCase() : String(id); }
输入输出:传 "abc" 得 "ABC",传 7 得 "7"。联合不是"模糊类型",它精确地限定了候选范围,比 any 安全得多。
把联合成员写成具体字面量,就得到一种枚举替代品,自带穷尽检查。
type Direction = "north" | "south" | "east" | "west"; function go(d: Direction) { // d 只能取四个值之一,拼错立刻标红 } go("north"); // OK go("up"); // 报错:'up' 不在候选里
背景:用字符串传方向,拼写错只能在运行时发现。操作:定义字面量联合。结果:拼错编译期就拦。解读:这比 enum 更轻量、编译后无额外对象。变式:配合 const 断言 as const 可锁死字面量,不让它拓宽成 string。
const dirs = ["north", "south", "east", "west"] as const; type Dir = typeof dirs[number]; // "north"|"south"|"east"|"west"
交叉描述"同时具备多组特征",结果类型拥有所有成员的全部字段。
interface HasName { name: string; } interface HasAge { age: number; } type Person = HasName & HasAge; // { name: string; age: number; } const p: Person = { name: "张三", age: 20 }; // 两个字段都要
输入输出:缺任一字段都报错。注意:若两成员有同名但类型冲突的属性,交叉结果该属性变成 never(见下)。
嵌套时遵循集合运算的分配律,但又不完全一样。看个容易算错的例子:
type A = { x: 1 } | { x: 2 }; type B = { y: "a" } | { y: "b" }; type AB = A & B; // 展开等价于四个组合之一: // { x:1; y:"a" } | { x:1; y:"b" } | { x:2; y:"a" } | { x:2; y:"b" }
交叉把"或"的每一侧与另一侧逐一组合,所以联合在交叉下被"打散"成多个成员。这张图表达这个展开:

交叉里若同名属性类型不兼容,该属性被推为 never,整个类型实际不可构造。
type X = { tag: "a" } & { tag: "b" }; // tag 是 "a" & "b" = never const x: X = { tag: "a" }; // 报错:tag 不能是 "a"(应为 never)
这常用于"排除某些组合"的模式,但更多是提醒你:交叉不是简单拼接,同名键会合并类型。
type ApiResponse<T> = | { ok: true; code: 200; data: T } | { ok: false; code: number; error: string }; function onResponse<T>(r: ApiResponse<T>) { if (r.ok) { console.log(r.data); // 收窄后 data 可用 } else { console.error(r.error); // 收窄后 error 可用 } }
背景:后端响应结构和错误结构不同。操作:用带 ok tag 的联合描述。结果:调用方在编译期被迫区分成功与失败。解读:这是可辨识联合 + 泛型(第三章)的组合,后面会常用。
把交叉和泛型结合,可以做"基础字段 + 场景字段"的可复用模型,避免每个接口重复声明 id/createdAt。
interface Base { id: number; createdAt: Date; } type WithMeta<T> = T & Base; interface Article { title: string; body: string; } type ArticleRecord = WithMeta<Article>; // 等价于 { id: number; createdAt: Date; title: string; body: string } const a: ArticleRecord = { id: 1, createdAt: new Date(), title: "强类型实战", body: "正文", };
背景:后端实体几乎都带 id 与审计字段。操作:抽成 Base 再用交叉注入。结果:每类实体只写业务字段,元字段一处维护。解读:交叉在这里是"混入"(mixin)的纯类型版,零运行时成本,比继承更灵活。
条件类型遇到联合会"分发"到每个成员上分别计算,这是第五章会深用的特性,这里先建立直觉。
type ToArray<T> = T extends any ? T[] : never; type R1 = ToArray<string | number>; // 等价于 string[] | number[],而非 (string | number)[] // 因为 string 与 number 被分别套用了 T[]
若想阻止分发(让整体当成一个类型),把 T 包进元组:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type R2 = ToArrayNonDist<string | number>; // (string | number)[]
背景:想把联合整体映射成某种容器时,分不清"逐成员"还是"整体"会写出预期外的类型。操作:用元组包裹关掉分发。结果:R1 是成员各自成数组,R2 是整体一个数组。解读:这条"分发开关"是类型编程的核心开关之一,第五章条件类型会反复用到。
表示"几种可能",联合通常比继承树更扁平、更易加新变体(加一个成员即可,不用改基类)。代价是消费端要写 switch。当变体之间几乎无共享行为时,联合优于继承;有强共享行为时,类层次更合适。
| 是候选集,只能调共有成员,需收窄& 合并所有字段,同名冲突键变 never⚠️ 别把交叉当成"对象合并"。它合并的是类型,且同名键取交集;若键类型冲突会得到 never,不是后者覆盖前者。
💡 状态、方向、响应这类"有限候选"优先用字面量联合,比字符串常量省心,比 enum 轻,还能白嫖穷尽检查。