Menu

TypeScript 类型守卫:检查对象的类型

类型守卫是 TypeScript 能看懂的运行时检查。本文讲解内置的类型守卫、如何用 value is Type 类型谓词编写自己的守卫、如何检查对象是否属于某个类型、用 asserts 写断言函数,以及如何校验 unknown 数据。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

类型守卫是 TypeScript 能看懂的运行时检查,因此它会在通过检查的分支里收窄类型。typeof、instanceof 和 in 是内置的守卫;其他情况就写一个返回类型为类型谓词 value is Type 的函数。

isUser 在运行时返回的只是普通的布尔值。返回类型 value is User 告诉编译器结果为 true 时能证明什么,之后每个 if (isUser(x)) 都会把 x 收窄为 User。

内置类型守卫

下面这些检查不需要任何辅助函数就能收窄类型:

守卫示例适用于
typeoftypeof x === "number"原始类型和函数
instanceofx instanceof Date类的实例
in"email" in x对象联合类型、unknown 对象的属性
Array.isArrayArray.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 或一条消息),你需要知道它是否符合你的接口。做法如下:

  1. typeof value === "object" && value !== null(是对象,而不是 null)。
  2. 对每个必需属性用 "prop" in value。在 unknown 上,in 会把该属性以 unknown 类型加到类型里。
  3. 对每个属性的类型用 typeof value.prop === "..."(或嵌套的守卫)。
  4. 数组用 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 库可以从同一个定义同时生成检查逻辑和类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始