第4章:类型建模 —— 联合、交叉、字面量与类型收窄
约 4 分钟 · 更新于 2026-09-01
第4章:类型建模 —— 联合、交叉、字面量与类型收窄
TypeScript 的核心能力不是给变量贴标签,而是把业务状态建模为可分析的类型。本章重点学习判别联合、类型守卫和穷尽检查,让非法状态更难进入程序。
一、从布尔字段陷阱开始
下面的模型允许大量矛盾状态:
ts
interface RequestState {
loading: boolean;
success: boolean;
error?: string;
data?: string[];
}
例如 loading 与 success 同时为 true,或者成功却没有 data。
改为判别联合:
ts
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };
每个状态只携带它真正需要的数据。
二、字面量类型
ts
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RetryCount = 0 | 1 | 2 | 3;
字面量类型适合表达有限状态、协议值和配置选项。配合 as const 可从运行时数据生成类型:
ts
const cabins = ["economy", "business", "first"] as const;
type Cabin = (typeof cabins)[number];
此时运行时数组和编译时联合来自同一来源,减少重复。
三、类型别名
ts
type UserId = string;
type Coordinates = readonly [number, number];
type Callback<T> = (value: T) => void;
类型别名不会创造新的名义类型:
ts
type UserId = string;
type OrderId = string;
// 两者仍然兼容
需要区分时可以采用品牌类型:
ts
type Brand<T, Name extends string> = T & { readonly __brand: Name };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
品牌通常应通过受控构造函数产生,而不是到处断言。
四、联合类型
ts
type Identifier = string | number;
对联合值只能直接使用所有成员共有的能力。要使用成员专属方法,先收窄。
ts
function formatId(id: Identifier): string {
return typeof id === "number" ? id.toFixed(0) : id.toUpperCase();
}
五、交叉类型
交叉类型表示同时满足多个类型:
ts
type Timestamped = { createdAt: Date; updatedAt: Date };
type User = { id: string; name: string };
type StoredUser = User & Timestamped;
交叉不是对象值的运行时合并。若同名属性无法兼容,结果可能变成 never:
ts
type Impossible = { id: string } & { id: number };
六、内置类型守卫
typeof
适用于原始类型:
ts
if (typeof value === "string") {
value.toUpperCase();
}
注意 typeof null === "object" 是 JavaScript 历史行为。
instanceof
适用于运行时类和构造函数:
ts
if (error instanceof Error) {
console.error(error.message);
}
接口不能用于 instanceof,因为运行时不存在。
in
ts
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(animal: Cat | Dog): void {
if ("meow" in animal) animal.meow();
else animal.bark();
}
真值检查
真值检查方便,但可能误伤空字符串或数字 0。若它们是合法值,应明确比较 null 和 undefined。
七、自定义类型谓词
ts
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.id === "number" &&
typeof candidate.name === "string";
}
类型谓词 value is User 是一项承诺。函数实现写错时,编译器不会替你证明它真实可靠,因此必须测试边界输入。
八、判别联合与穷尽检查
ts
type PaymentResult =
| { kind: "success"; transactionId: string }
| { kind: "rejected"; reason: string }
| { kind: "pending"; pollUrl: string };
function describe(result: PaymentResult): string {
switch (result.kind) {
case "success":
return `成功:${result.transactionId}`;
case "rejected":
return `拒绝:${result.reason}`;
case "pending":
return `处理中:${result.pollUrl}`;
default:
return assertNever(result);
}
}
function assertNever(value: never): never {
throw new Error(`未处理状态:${JSON.stringify(value)}`);
}
新增成员后,未更新的 switch 会产生类型错误。
九、类型断言的真实含义
ts
const element = document.querySelector("#app") as HTMLDivElement;
断言不会查询 DOM,也不会保证元素存在。更安全的写法:
ts
const element = document.querySelector("#app");
if (!(element instanceof HTMLDivElement)) {
throw new Error("#app 不是 div");
}
双重断言 value as unknown as Target 基本是在绕过类型系统,应视为高风险信号。
十、satisfies:检查但保留精确类型
ts
const routes = {
home: "/",
user: "/users/:id"
} satisfies Record<string, string>;
与直接标注相比,satisfies 会检查对象符合目标约束,同时保留对象自身更精确的键和值信息。
十一、递归类型
ts
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
递归类型适合树、菜单、JSON 和嵌套评论。但深层类型运算会增加编译器负担,应优先可读性,不要把类型系统当通用编程语言无限递归。
十二、协变与逆变的实用理解
不必一开始背理论,先记住函数参数和返回值方向不同:
- 返回值更具体通常可以赋给返回值更宽泛的位置。
- 参数类型不能随意变窄,否则调用者可能传入实现无法处理的值。
ts
class Animal {}
class Dog extends Animal { bark() {} }
let createAnimal: () => Animal;
const createDog = (): Dog => new Dog();
createAnimal = createDog;
严格函数类型会帮助检查回调参数的不安全替换。
十三、常见误区
- 用多个布尔值表达互斥状态。
- 认为类型别名会创造新的运行时类型。
- 把交叉类型理解为 Object.assign。
- 自定义类型守卫内部随意断言。
- 用 as 消除所有报错,而不处理数据来源。
- 真值检查误把 0 或空字符串当缺失。
- 判别联合没有 never 穷尽检查。
- 设计过度复杂的递归类型,导致编译慢且错误难读。
十四、实践练习
- 把“加载/成功/失败”四字段模型改成判别联合。
- 为航班舱等建立 as const 数据源和派生联合类型。
- 编写 isUser,测试 null、数组、缺字段和错误字段类型。
- 为支付结果实现穷尽处理,新增 cancelled 后观察错误。
- 用品牌类型区分 UserId 与 OrderId,建立构造函数。
十五、总结
- 类型建模的目标是减少非法状态,而不是追求复杂语法。
- 字面量和联合类型适合有限状态与协议值。
- 判别联合让控制流、数据和穷尽检查协同工作。
- 类型守卫连接运行时检查与静态收窄。
- 类型断言只是信任声明,不是校验和转换。
- 交叉、递归和型变需要服务于真实模型,避免为高级而高级。
请继续阅读:第5章:集合、异步与错误边界。
原始资料引用