类型守卫与类型收窄(下)
本教程共 80 篇 · 第 52 篇 · 更新于 2026-08-10 · 约 14 分钟阅读
本节目标:学会写自定义类型守卫函数(
is关键字)和断言函数(asserts关键字),掌握判别联合(discriminated union)模式——这是实际项目中处理联合类型的最强方案。
自定义类型守卫:is 关键字
前面讲的 typeof、instanceof、in 都是 TypeScript 内置识别的守卫。但如果你的判断逻辑更复杂——比如一个对象是否满足某个复杂条件——就需要自定义类型守卫。
自定义类型守卫的返回类型是 parameterName is Type,这叫类型谓词(type predicate):
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet: Fish | Bird = getPet();
if (isFish(pet)) {
pet.swim(); // ✅ pet 被收窄为 Fish
} else {
pet.fly(); // ✅ pet 被收窄为 Bird
}
注意两点:
pet is Fish中的pet必须是函数的某个参数名- 函数体里返回
true/false的逻辑你自己负责——TypeScript 不会验证守卫逻辑是否正确,它无条件信任你的类型谓词
与 filter 配合
自定义守卫在 Array.filter 中特别有用:
const zoo: (Fish | Bird)[] = [getPet(), getPet(), getPet()];
const fishTank: Fish[] = zoo.filter(isFish);
// TypeScript 知道 filter(isFish) 返回的是 Fish[]
断言函数:asserts 关键字
断言函数和自定义守卫类似,但它在条件不满足时抛出错误,而不是返回 boolean。返回类型写 asserts value is Type:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processValue(value: unknown) {
assertIsString(value);
// 之后的所有代码中,value 被收窄为 string
console.log(value.toUpperCase());
}
和自定义守卫的区别:
- 自定义守卫用
if做分支,两个分支类型不同 - 断言函数不需要
if,调用后当前作用域的类型直接被改写
这个模式常用于验证函数参数的合法性——在函数开头做断言,后续代码就不用反复检查类型了。
判别联合(Discriminated Union)
这是本章最重要的部分,也是实际项目中处理联合类型的最佳实践。
一个失败的设计
假设你要表示圆形和正方形两种图形,最直观的做法是把所有字段放在一个类型里:
interface Shape {
kind: "circle" | "square";
radius?: number; // 圆形有半径
sideLength?: number; // 正方形有边长
}
然后根据 kind 来判断当前是哪种形状:
function getArea(shape: Shape) {
if (shape.kind === "circle") {
// ❌ shape.radius 仍然是 number | undefined
// TypeScript 不知道 radius 和 kind 之间的关联
return Math.PI * shape.radius ** 2;
}
}
问题在于:kind 是 "circle" 时 radius 逻辑上一定存在,但 TypeScript 的类型系统看不出来这个关系。
正确的设计:把每个变体拆成独立类型
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
关键变化:kind 不再是宽泛的 "circle" | "square",而是每个接口各自的字面量。这告诉 TypeScript:kind 为 "circle" 的一定是 Circle,kind 为 "square" 的一定是 Square。
现在 getArea 就完全类型安全了:
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // ✅ shape 收窄为 Circle
case "square":
return shape.sideLength ** 2; // ✅ shape 收窄为 Square
}
}
kind 在这里的专业术语叫判别属性(discriminant),Shape 就是判别联合(discriminated union / tagged union)。“判别联合”这个名字强调的不是联合类型本身,而是**“用一个公共字面量字段来区分联合中的所有成员”**的模式。
判别联合的三要素
- 公共属性:所有成员类型共用一个同名字段(通常是
kind、type、status等) - 字面量类型:每个成员的该字段是唯一的字面量类型
- 独立类型:每个成员是一个独立的
interface或type
典型应用场景
判别联合在前端状态管理中极常用:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; message: string };
function render(state: RequestState) {
switch (state.status) {
case "idle":
return "等待请求...";
case "loading":
return "加载中...";
case "success":
// state 收窄为 { status: "success"; data: string }
return `数据: ${state.data}`;
case "error":
// state 收窄为 { status: "error"; message: string }
return `错误: ${state.message}`;
}
}
每个状态的附带数据都不一样,但 status 字段安全地标识了当前到底是哪种状态。这比用一堆可选的 data?、message? 字段要清晰和安全得多。
never 穷尽性检查
判别联合最大的好处之一:你可以确保 switch 处理了所有分支。
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// 如果所有分支都已处理,shape 的类型在这里是 never
const _exhaustive: never = shape;
return _exhaustive;
}
}
never 只能赋给 never。如果所有 Shape 的成员都已经被 case 覆盖了,default 分支中 shape 的类型就是 never,赋值给 _exhaustive: never 不会报错。
但如果未来给 Shape 加了新成员:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
getArea 函数的 default 分支就会报错:
Type 'Triangle' is not assignable to type 'never'.
这个编译错误就是在提醒你:“嘿,你漏了 Triangle 的分支没处理。“这种写法叫穷尽性检查(exhaustiveness checking)。
小结
- 自定义类型守卫返回
param is Type,让复杂判断也能参与收窄 - 断言函数写
asserts value is Type,抛出错误来改写作用域内的类型 - 判别联合把 union 的每个变体拆成独立类型,用字面量字段区分——这是处理联合类型的黄金模式
never用于不在default分支的穷尽性检查,防止遗漏新增的变体