TypeScript 中的类型守卫与断言函数:编写更安全的运行时检查

TypeScript 的静态类型系统能在编译时捕获大量错误,但当数据来自外部源——API 响应、用户输入、JSON.parse 的结果——编译器对你的类型声明只能选择信任。这种信任在运行时可能被轻易打破,而类型守卫和断言函数正是弥合这一鸿沟的关键工具。它们让你在运行时验证数据,并将验证结果反馈给类型系统,从而在类型安全与运行时安全之间建立真正的连接。

为什么需要运行时类型检查

考虑一个常见场景:

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // 返回类型是 any,但被断言为 User
}

res.json() 的返回类型是 Promise<any>,赋值给 Promise<User> 不会产生编译错误。如果服务端返回了 { id: "1", name: null },TypeScript 不会阻止你,但运行时访问 user.name.toUpperCase() 就会崩溃。类型守卫的价值就在于:把这种隐式的、危险的信任,替换为显式的、可验证的检查。

内置类型守卫:typeof、instanceof 与 in

TypeScript 能识别几种特定的运行时检查语法,并据此收窄类型。

typeof 守卫

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // 此分支中 value 被收窄为 string
  }
  return value.toFixed(2); // 此分支中 value 被收窄为 number
}

typeof 适用于原始类型判断,但无法区分对象的具体形状,也无法处理 nulltypeof null === "object")。

instanceof 守卫

function logDate(value: Date | string): void {
  if (value instanceof Date) {
    console.log(value.toISOString());
  } else {
    console.log(value); // value 被收窄为 string
  }
}

instanceof 依赖原型链,在跨 iframe 或使用自定义类时需谨慎。

in 守卫

interface Admin {
  role: "admin";
  permissions: string[];
}

interface Member {
  role: "member";
  joinedAt: Date;
}

function describe(user: Admin | Member): string {
  if ("permissions" in user) {
    return `管理员,权限数:${user.permissions.length}`;
  }
  return `成员,加入时间:${user.joinedAt.toISOString()}`;
}

in 操作符检查属性是否存在,适合区分具有不同属性集的对象类型。

自定义类型守卫

内置守卫能力有限。当你需要验证复杂对象结构时,需要编写自定义类型守卫。其核心是返回类型谓词 value is Type

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof (value as User).id === "number" &&
    "name" in value &&
    typeof (value as User).name === "string" &&
    "email" in value &&
    typeof (value as User).email === "string"
  );
}

返回类型 value is User 告诉 TypeScript:当这个函数返回 true 时,参数 value 的类型可以收窄为 User。使用方式非常自然:

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const data: unknown = await res.json();
  if (!isUser(data)) {
    throw new Error("Invalid user data");
  }
  return data; // data 的类型已被收窄为 User
}

注意 data 被显式声明为 unknown 而非 anyunknown 是类型安全的顶层类型,强制你在使用前进行验证,而 any 会绕过所有检查。在运行时验证场景中,始终优先使用 unknown

断言函数

断言函数与类型守卫密切相关,区别在于:类型守卫返回布尔值,断言函数则在不满足条件时抛出异常。其返回类型为 asserts value is Type

function assertIsUser(value: unknown): asserts value is User {
  if (
    typeof value !== "object" ||
    value === null ||
    typeof (value as User).id !== "number" ||
    typeof (value as User).name !== "string" ||
    typeof (value as User).email !== "string"
  ) {
    throw new Error("Expected a valid User object");
  }
}

调用断言函数后,TypeScript 会自动收窄后续代码中变量的类型:

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const data: unknown = await res.json();
  assertIsUser(data);
  return data; // data 的类型已收窄为 User,无需 if 分支
}

断言函数特别适合在函数入口处做前置条件检查,让主逻辑保持扁平,避免深层嵌套的 if 语句。

断言函数的注意事项

断言函数有一个需要警惕的特性:如果被断言的值来自一个被显式注解为宽类型的变量,TypeScript 在某些情况下可能无法正确收窄。例如:

const data: unknown = getData();
assertIsUser(data);
// data 现在是 User

const obj: object = {};
assertIsUser(obj); // 编译错误?取决于具体版本和上下文

此外,断言函数要求被断言的参数必须是显式的引用(变量、属性访问等),不能是表达式。这意味着 assertIsUser(await res.json()) 不会收窄任何东西,因为结果没有被绑定到变量。

类型守卫与断言函数的选型

两者并非互斥,选择取决于使用场景:

  • 类型守卫适合需要在条件分支中处理不同情况的场景,比如联合类型的分发、数组过滤(filter(isUser) 能正确推导出 User[])。
  • 断言函数适合“不满足条件就是错误”的场景,比如函数参数校验、配置对象验证。它让主流程更线性,减少缩进层级。

一个实用的组合模式是在底层提供类型守卫,在上层封装断言函数:

function isUser(value: unknown): value is User { /* ... */ }

function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error("Invalid user data");
  }
}

这样既保留了布尔判断的灵活性,又提供了断言式的简洁调用。

实战建议

优先使用 unknown 而非 anyunknown 强制你在使用前验证,而 any 会静默传播,让类型守卫形同虚设。

类型守卫应尽可能严格。上面的 isUser 检查了每个属性的存在性和类型。如果只检查 "id" in value,那么 { id: "abc" } 也会通过,后续使用 user.id + 1 就会产生意外结果。验证的粒度应与类型定义的严格程度匹配。

考虑使用验证库。手写类型守卫在类型复杂时容易遗漏。Zod、Valibot 等库能从 schema 自动推导 TypeScript 类型,并生成运行时验证函数,减少手写守卫的维护成本。但理解类型守卫和断言函数的原理仍然重要——这些库的底层正是基于 value is Typeasserts value is Type 实现的。

避免过度断言。类型守卫和断言函数只应在数据确实来自不可信来源时使用。对于内部函数间的数据传递,如果类型系统已经能保证正确性,额外的运行时检查只会增加开销和噪音。

类型守卫和断言函数将 TypeScript 的类型能力从编译时延伸到了运行时。它们让你能够以类型安全的方式处理外部数据,在保持代码可读性的同时,消除因类型假设错误而导致的运行时崩溃。掌握这两种工具,是从“会用 TypeScript”到“用好 TypeScript”的重要一步。

未经允许不得转载:任鹏个人博客 » TypeScript 中的类型守卫与断言函数:编写更安全的运行时检查

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏