CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
联合与交叉类型
这一课你将能做到
用联合类型 | 表达「多选一」,用交叉类型 & 表达「全都要」
用字面量联合替代魔法字符串,写出精确的状态字段
掌握判别联合(Discriminated Unions)建模「不同形态的数据」,这是业务开发中最实用的类型技巧
为什么要学
真实世界的数据很少是单一形状的:一个接口可能返回成功或失败两种结构;一个字段可能是 string 也可能是 number;一个组件状态可能是「加载中 / 成功 / 失败」三选一。
联合类型 | 让你精确表达「这个值是这几种类型之一」;交叉类型 & 让你把多个类型合并成一个。而判别联合 是 TS 建模的灵魂——后端返回的复杂 JSON、前端的多状态 UI,用判别联合建模后,编译器能帮你在每个分支里精确推导出可用字段,写错字段名当场报错。
先建立图像
两个运算符的方向感:
联合 A | B: 「或者」——值是 A 或 B 之一(集合的并集)
┌─────┐ ┌─────┐
│ A │ ∪ │ B │ 取值范围变【宽】
└─────┘ └─────┘ 可用能力变【窄】(只有共有成员)
交叉 A & B: 「并且」——值同时满足 A 和 B(形状的叠加)
┌─────────────┐
│ A 的字段 │
│ + │ 取值范围变【窄】(要同时满足)
│ B 的字段 │ 可用能力变【宽】(字段全都有)
└─────────────┘
记忆口诀:联合取值变宽、能力变窄;交叉取值变窄、能力变宽。
跟着做
新建 unions-demo.ts,逐步实操。
步骤 1:联合类型基础
// id 可能是数字或字符串(后端常见情况)
function printId(id: number | string): void {
console.log(`ID: ${id}`);
}
printId(101); // OK
printId("abc"); // OK
// printId(true); // 报错
// 联合类型的使用限制:只能调用所有成员共有的方法
function getLength(value: string | string[]): number {
// value.toUpperCase(); // 报错:string[] 没有这个方法
return value.length; // OK:string 和 string[] 都有 length
}
console.log(getLength("hello")); // 5
console.log(getLength(["a", "b"])); // 2
想使用某个成员特有的方法?先做类型收窄(下一课详讲):
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // 这个分支里 TS 知道 value 是 string
}
return value.toFixed(2); // 这里 value 是 number
}
console.log(format("hello"), format(3.14159)); // HELLO 3.14
步骤 2:字面量联合——替代魔法字符串
// 不要这样:status 是任意 string,拼错都不报错
// let status: string = "pensding"; // 笔误没人管
// 要这样:取值限定为三个字面量之一
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";
let status: OrderStatus = "pending";
status = "paid";
// status = "pensding"; // 报错:笔误当场被抓
function statusText(s: OrderStatus): string {
switch (s) {
case "pending": return "待支付";
case "paid": return "已支付";
case "shipped": return "已发货";
case "cancelled": return "已取消";
}
}
console.log(statusText(status)); // 已支付
字面量联合比枚举更轻量,是现代 TS 项目表达「固定取值集合」的首选。
步骤 3:交叉类型 & interface Timestamped {
createdAt: string;
updatedAt: string;
}
interface Named {
name: string;
}
// 同时满足两个形状
type NamedEntity = Timestamped & Named;
const item: NamedEntity = {
name: "测试条目",
createdAt: "2026-01-01",
updatedAt: "2026-10-06",
};
console.log(item.name, item.createdAt); // 测试条目 2026-01-01
注意交叉原始类型会得到 never(不可能同时是 string 又是 number):
type Impossible = string & number; // never,没意义
交叉类型的正确用法是合并对象形状 ,不是交叉原始值。
步骤 4:判别联合(重点中的重点) 核心技巧:给每个形态加一个共同的、取值互斥的字面量字段 (判别字段),TS 就能根据它自动收窄。
// 三种形状,都用 kind 字段做判别
interface LoadingState {
kind: "loading";
}
interface SuccessState {
kind: "success";
data: string[];
}
interface ErrorState {
kind: "error";
message: string;
}
// 合成判别联合
type FetchState = LoadingState | SuccessState | ErrorState;
function render(state: FetchState): string {
switch (state.kind) {
case "loading":
return "加载中...";
case "success":
// 这个分支里 TS 精确知道 data 存在
return `拿到 ${state.data.length} 条数据`;
case "error":
// 这里 message 存在,但访问 state.data 会报错
return `出错了:${state.message}`;
}
}
console.log(render({ kind: "loading" })); // 加载中...
console.log(render({ kind: "success", data: ["a", "b"] })); // 拿到 2 条数据
console.log(render({ kind: "error", message: "网络超时" })); // 出错了:网络超时
感受这个威力:state.kind === "success" 的分支里,state.data 必然存在,编译器保证;你敢访问 state.message 直接红线。状态机的每个分支能用什么字段,类型系统说了算。
步骤 5:判别联合实战——API 响应建模 // 后端接口的典型两种返回
interface ApiSuccess {
code: 0;
data: { username: string; balance: number };
}
interface ApiFailure {
code: 400 | 401 | 500;
error: string;
}
type ApiResponse = ApiSuccess | ApiFailure;
function handleResponse(res: ApiResponse): void {
if (res.code === 0) {
// TS 知道这里是 ApiSuccess
console.log(`欢迎 ${res.data.username},余额 ¥${res.data.balance}`);
} else {
// TS 知道这里是 ApiFailure
console.log(`请求失败(${res.code}):${res.error}`);
}
}
handleResponse({ code: 0, data: { username: "小明", balance: 99.5 } });
// 欢迎 小明,余额 ¥99.5
handleResponse({ code: 401, error: "登录已过期" });
// 请求失败(401):登录已过期
步骤 6:联合与交叉混合使用 interface BaseUser {
id: number;
name: string;
}
interface Admin {
role: "admin";
permissions: string[];
}
interface Member {
role: "member";
department: string;
}
// 交叉 + 联合:基础字段 × 角色判别联合
type User = BaseUser & (Admin | Member);
function describe(user: User): string {
if (user.role === "admin") {
return `${user.name} 是管理员,权限:${user.permissions.join(", ")}`;
}
return `${user.name} 是成员,部门:${user.department}`;
}
console.log(describe({ id: 1, name: "老板", role: "admin", permissions: ["all"] }));
// 老板 是管理员,权限:all
console.log(describe({ id: 2, name: "员工", role: "member", department: "技术部" }));
// 员工 是成员,部门:技术部
讲清楚要点
| 是类型的并集 :A | B 的值可以是 A 或 B;未收窄前只能访问两者共有的成员。
字面量联合优先于枚举 :"a" | "b" | "c" 零编译产物、和后端字符串字段天然对齐,大多数场景替代 enum。
判别联合三要素 :多个接口、一个共有的字面量判别字段(常用 kind/type/status)、一个联合类型把它们合起来。
判别字段必须是字面量类型 :kind: string 无法判别(太宽),必须 kind: "loading" 这样精确到值。
交叉类型用于对象合并 :& 叠加多个对象的字段;原始类型交叉基本只会得到 never,别这么写。
联合的分布式特性 :对联合类型做操作时 TS 会逐个成员检查,这是后面条件类型和工具类型的基础。
你可能会懵的地方
A | B 类型上能直接访问 A 的独有属性吗?
不能。编译器不知道此刻它是 A 还是 B,只放行共有成员。必须先收窄(if、switch、typeof),让 TS 确定当前是哪一个。
判别字段叫什么名字有讲究吗?
没有强制,社区惯用 kind、type、status。和后端对齐就行,关键是用字面量类型。
联合类型最多能写几个成员?
语法上没有硬限制,但超过 7-10 个就该审视设计:是不是该用判别联合拆成多个子形状,或者某些成员该合并。
string | null 和 string | undefined 和 string? 有什么区别?
前两者是类型层面允许空值;? 只用于对象属性和函数参数,表示「可以缺省」。prop?: string 约等于 prop: string | undefined 且键可不存在。
交叉类型字段冲突会怎样?
type X = { a: string } & { a: number } 中 a 变成 string & number 即 never——没有值能满足。字段冲突时优先用接口继承(冲突当场报错)而非交叉。
错法对照 错法 问题 正解 status: string 表示固定状态集任意字符串都合法,笔误不报错 字面量联合 "pending" | "paid" 联合类型直接调独有方法 编译报错 先 typeof/in/判别字段收窄 用可选属性堆叠表达多形态 {data?, error?} data 和 error 可同时存在或同时不存在,失去约束 判别联合:成功必有 data,失败必有 error string & number 期待「既是又是」结果是 never 交叉只用于对象形状合并 判别字段写成 kind: string 无法收窄 每个分支用精确字面量 kind: "loading"
卡点急救
报错 Property 'x' does not exist on type 'A | B' :你在访问非共有成员。确认是否已收窄;若逻辑上确定是某分支,用判别字段或 as 断言(后者要负责)。
switch 收窄不生效 :判别字段不是字面量类型(比如标成了 string),回接口里把 kind 改成精确字面量。
联合类型赋值报不兼容 :值不符合联合中任何一个成员的形状。对照每个成员检查缺了哪个字段、哪个字段类型不对。
A & B 推导出 never :两个类型的同名字段冲突。改用接口继承看冲突在哪,或重命名字段。
面试常问
联合类型和交叉类型的区别?
联合 |:值是成员类型之一,取值范围是并集,访问能力限于共有成员;交叉 &:值要同时满足所有成员,主要用于对象形状的合并叠加。
什么是判别联合?解决什么问题?
多个对象类型共享一个字面量判别字段,联合后 TS 可依据判别字段自动收窄到具体成员。解决「同一字段有多种形态数据」的建模问题,典型场景:API 成功/失败响应、UI 加载状态机、Redux 的 action。
type A = string | number 和 let a: string | number 能调什么方法?
只能调 string 和 number 共有的方法(如 toString、valueOf)。独有方法(toUpperCase、toFixed)必须先收窄。
字面量联合和枚举怎么选?
现代 TS 倾向字面量联合:零运行时产物、与 JSON 数据直接兼容、tree-shaking 友好。枚举的优势是反向映射和集中命名空间,适合需要运行时遍历取值的场景。
自测三问
为什么 A | B 类型的值不能直接调用 A 独有的方法?怎么才能调?
判别联合的三个必要组成部分是什么?
「成功响应有 data、失败响应有 error、两者不会同时出现」——用可选属性和用判别联合建模,哪个更安全?为什么?
小结一句话 | 表达「多选一」、& 表达「全都要」、字面量联合圈定取值、判别联合让编译器替你把关每个分支——掌握这四样,业务数据建模就通关了。
延伸