类型守卫是 TypeScript 能看懂的运行时检查,因此它会在通过检查的分支里收窄类型。typeof、instanceof 和 in 是内置的守卫;其他情况就写一个返回类型为类型谓词 value is Type 的函数。
isUser 在运行时返回的只是普通的布尔值。返回类型 value is User 告诉编译器结果为 true 时能证明什么,之后每个 if (isUser(x)) 都会把 x 收窄为 User。
内置类型守卫
下面这些检查不需要任何辅助函数就能收窄类型:
| 守卫 | 示例 | 适用于 |
|---|---|---|
typeof | typeof x === "number" | 原始类型和函数 |
instanceof | x instanceof Date | 类的实例 |
in | "email" in x | 对象联合类型、unknown 对象的属性 |
Array.isArray | Array.isArray(x) | 数组 |
| 相等比较 | x === null、x.kind === "circle" | null/undefined、字面量标签 |
| 真值判断 | if (x) | 排除 null 和 undefined |
它们都作为普通的 JavaScript 运行。TypeScript 额外做的是收窄:它读懂检查,并调整每个分支里的类型。全部收窄形式见类型收窄页面。自定义守卫适用于一个表达式写不下、或者需要复用的检查。
编写类型谓词
类型谓词的形式是 parameterName is Type,用来代替返回类型 boolean。收窄在两个方向上都生效:true 收窄为 Type,false 则从联合类型中去掉 Type。
把守卫传给 filter,得到的数组类型就是正确的。从 TypeScript 5.5 开始,编译器还会从简单的箭头函数推断出谓词,所以 pets.filter((p) => p.kind === "cat") 不用具名守卫也会返回 Cat[]。
谓词中的类型必须能赋值给参数的类型:function f(x: string): x is number 会报错 TS2677,A type predicate's type must be assignable to its parameter's type.
编译器信任你的守卫
TypeScript 会检查守卫是否返回布尔值,但不会检查这个布尔值对不对。一个对错误的值返回 true 的守卫会让类型“说谎”,程序在运行时出错,而编译时没有任何错误。
data.price.toFixed(2) 在运行时抛出 TypeError: Cannot read properties of undefined (reading 'toFixed')。编译器之所以把 data.price 当作 number,是因为守卫这么说了。后续代码依赖的每个属性都要检查,并让守卫保持短小、经过测试,并放在它所描述的类型旁边。
检查对象是否属于某个类型
大多数自定义守卫都是为了回答这个问题:数据以 unknown 的形式到来(来自 JSON.parse、fetch、localStorage 或一条消息),你需要知道它是否符合你的接口。做法如下:
typeof value === "object" && value !== null(是对象,而不是null)。- 对每个必需属性用
"prop" in value。在unknown上,in会把该属性以unknown类型加到类型里。 - 对每个属性的类型用
typeof value.prop === "..."(或嵌套的守卫)。 - 数组用
Array.isArray(value.items) && value.items.every(isItem)。
结构较大或嵌套较深时,手写这些检查很繁琐。Zod、Valibot 这类 schema 库让你只描述一次结构,就能同时得到运行时检查和 TypeScript 类型。
断言函数:asserts value is Type
断言函数在检查失败时抛出异常,否则正常返回。它的返回类型是 asserts value is Type(或 asserts condition),调用之后的所有代码都会被收窄,不需要 if。
有一条规则经常让人栽跟头:断言函数必须通过带有显式类型的名字来调用。没有注解的 const 箭头函数,比如 const check = (v: unknown): asserts v is string => {...},会在调用处报错 TS2775,Assertions require every name in the call target to be declared with an explicit type annotation. 请改用 function 声明,或者给常量加上函数类型注解。
守卫、断言与类型断言对比
| 工具 | 运行时检查? | 收窄范围 | 失败时 |
|---|---|---|---|
内置守卫(typeof、in...) | 是 | 分支内部 | 走另一个分支 |
value is T 函数 | 是(你的代码) | 分支内部 | 走另一个分支 |
asserts value is T 函数 | 是(你的代码) | 调用之后 | 抛出异常 |
value as T | 否 | 该表达式 | 什么也不发生:错误的类型会扩散 |
类型断言(as)只改变类型,不做任何检查。在数据来自外部的边界上,守卫或断言函数才是同一想法的安全版本。
类中基于 this 的守卫
方法可以用 this is Type 收窄调用它的对象,这在类的继承层次中很方便:
class FileNode {
constructor(public name: string) {}
isDirectory(): this is DirectoryNode {
return this instanceof DirectoryNode;
}
}
class DirectoryNode extends FileNode {
children: FileNode[] = [];
}
function count(node: FileNode): number {
return node.isDirectory() ? node.children.length : 0; // node: DirectoryNode in the true branch
}
常见问题
TypeScript 中的类型守卫是什么?
任何被 TypeScript 用来收窄类型的运行时检查都是类型守卫:typeof x === "string"、x instanceof Date、"id" in x、Array.isArray(x),或者调用一个返回类型为类型谓词(如 x is User)的函数。在通过检查的分支里,变量的类型会变得更窄。
TypeScript 中如何检查一个对象是否属于某个类型?
类型在运行时并不存在,所以要检查的是属性:写一个函数 isUser(value: unknown): value is User,用它检查 typeof value === "object"、value !== null,再用 in 和 typeof 逐个检查必需的属性。if (isUser(x)) 之后,x 的类型就是 User。如果是类,用 x instanceof MyClass 就够了。
TypeScript 中的“value is Type”是什么意思?
这是类型谓词,用作函数的返回类型。函数在运行时仍然返回布尔值,但返回 true 时,TypeScript 会在调用处把实参收窄为 Type;返回 false 时,则收窄为联合类型中的其余成员。编译器不会验证函数体,所以检查本身必须写对。
类型守卫和断言函数有什么区别?
类型守卫(x is T)返回布尔值,在 if 内部收窄类型。断言函数(asserts x is T)不返回任何值,检查失败时抛出异常,所以调用之后的所有代码都已收窄,不需要 if。需要分支时用类型守卫,表达“必须成立,否则就停下”时用断言函数。
TypeScript 能检查对象是否实现了某个接口吗?
不能直接检查:接口在编译后会被擦除,instanceof 也不接受接口。可以写一个检查接口属性的类型守卫,或者加一个字面量标签属性(kind: "user")再比较它。Zod 这类 schema 库可以从同一个定义同时生成检查逻辑和类型。