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 适用于原始类型判断,但无法区分对象的具体形状,也无法处理 null(typeof 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 而非 any。unknown 是类型安全的顶层类型,强制你在使用前进行验证,而 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 而非 any。unknown 强制你在使用前验证,而 any 会静默传播,让类型守卫形同虚设。
类型守卫应尽可能严格。上面的 isUser 检查了每个属性的存在性和类型。如果只检查 "id" in value,那么 { id: "abc" } 也会通过,后续使用 user.id + 1 就会产生意外结果。验证的粒度应与类型定义的严格程度匹配。
考虑使用验证库。手写类型守卫在类型复杂时容易遗漏。Zod、Valibot 等库能从 schema 自动推导 TypeScript 类型,并生成运行时验证函数,减少手写守卫的维护成本。但理解类型守卫和断言函数的原理仍然重要——这些库的底层正是基于 value is Type 和 asserts value is Type 实现的。
避免过度断言。类型守卫和断言函数只应在数据确实来自不可信来源时使用。对于内部函数间的数据传递,如果类型系统已经能保证正确性,额外的运行时检查只会增加开销和噪音。
类型守卫和断言函数将 TypeScript 的类型能力从编译时延伸到了运行时。它们让你能够以类型安全的方式处理外部数据,在保持代码可读性的同时,消除因类型假设错误而导致的运行时崩溃。掌握这两种工具,是从“会用 TypeScript”到“用好 TypeScript”的重要一步。
未经允许不得转载:任鹏个人博客 » TypeScript 中的类型守卫与断言函数:编写更安全的运行时检查


朋友圈点赞图在线生成源码