首页 / TypeScript 入门教程 / 类型守卫与类型收窄(下)

TypeScript 入门教程

类型守卫与类型收窄(下)

本教程共 80 篇 · 第 52 篇 · 更新于 2026-08-10 · 约 14 分钟阅读

TypeScriptTypeScript 入门教程类型守卫类型收窄Discriminated Unionnever

本节目标:学会写自定义类型守卫函数(is 关键字)和断言函数(asserts 关键字),掌握判别联合(discriminated union)模式——这是实际项目中处理联合类型的最强方案。

自定义类型守卫:is 关键字

前面讲的 typeofinstanceofin 都是 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" 的一定是 Circlekind"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)。“判别联合”这个名字强调的不是联合类型本身,而是**“用一个公共字面量字段来区分联合中的所有成员”**的模式。

判别联合的三要素

  1. 公共属性:所有成员类型共用一个同名字段(通常是 kindtypestatus 等)
  2. 字面量类型:每个成员的该字段是唯一的字面量类型
  3. 独立类型:每个成员是一个独立的 interfacetype

典型应用场景

判别联合在前端状态管理中极常用:

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 分支的穷尽性检查,防止遗漏新增的变体