类型守卫(type guard)是 TypeScript 控制流分析的核心机制。它让编译器在特定代码路径里把一个宽类型窄化成更具体的类型。
很多人会用 typeof 和 instanceof,但到了需要自定义守卫的时候就卡住了——不知道怎么写类型谓词,不理解 asserts 签名的用途,不清楚 in 操作符的窄化规则。
内置守卫
TypeScript 编译器内置了对几个 JavaScript 操作符的类型窄化支持。
typeof
typeof 是最基础的窄化方式,但它的局限性很明显——只能区分 primitive 类型:
function format(value: string | number | Date): string {
if (typeof value === 'string') return value.trim()
if (typeof value === 'number') return value.toFixed(2)
// 这里 value 是 Date
return value.toISOString()
}
typeof 能识别的类型只有:string、number、boolean、symbol、bigint、undefined、function、object。注意 null 也会被 typeof 报为 'object',这是一个历史遗留 bug,但 TypeScript 在窄化时做了修正——typeof x === 'object' 会排除 null。
instanceof
instanceof 用原型链检查来窄化对象类型:
class ApiError {
constructor(public code: number, public message: string) {}
}
class NetworkError {
constructor(public message: string, public retryable: boolean) {}
}
function handleError(err: ApiError | NetworkError) {
if (err instanceof ApiError) {
// err: ApiError
console.log(`API Error ${err.code}: ${err.message}`)
} else {
// err: NetworkError
}
}
instanceof 的窄化基于原型链——不是基于结构兼容性。所以两个没有继承关系的类,即使结构相同也不会相互匹配。
in
in 操作符检查属性是否存在,也能触发窄化:
type Dog = { bark: () => void }
type Cat = { meow: () => void }
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
// animal: Dog —— 因为只有 Dog 有 bark 方法
animal.bark()
} else {
// animal: Cat
animal.meow()
}
}
in 的窄化规则略有不同:如果被检查的属性只在联合的一个成员中出现,那就窄化到那个成员。如果属性在多个成员中都存在(即使类型不同),in 不会产生窄化。
自定义类型谓词
当内置守卫不够用时,可以写自定义类型守卫。核心是函数的返回类型写成 x is Type 的形式——这就是类型谓词(type predicate):
interface User { kind: 'user'; name: string; email: string }
interface Admin { kind: 'admin'; name: string; role: string; permissions: string[] }
function isAdmin(person: User | Admin): person is Admin {
return (person as Admin).permissions !== undefined
}
function handlePerson(person: User | Admin) {
if (isAdmin(person)) {
// person: Admin
console.log(person.role)
}
}
类型谓词 person is Admin 告诉编译器:如果这个函数返回 true,那么入参的类型就收窄到 Admin。这相当于一个编译器的信任声明——它不检查函数体是否正确实现了这个断言。
类型谓词的常见陷阱
// 错误的守卫——检查的是 user,却说 is Admin
function isAdminWrong(person: User | Admin): person is Admin {
return (person as User).email !== undefined // 实际检查的是 email
}
编译器不会报错。类型谓词是开发者对编译器的一个承诺——你必须保证实现是正确的。写类型谓词时,测试覆盖率比类型安全更重要。
断言函数与断言签名
断言函数(assertion function)是类型守卫的另一种形式。它不返回 boolean,而是在条件不满足时抛出错误:
function assertString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`)
}
}
function process(input: unknown) {
assertString(input)
// input: string —— 如果执行到了这里,断言必然满足
console.log(input.toUpperCase())
}
asserts value is string 的含义是:这个函数要么抛异常,要么把 value 的类型收窄到 string。
断言函数的实际价值是在数据验证场景:
interface Config {
host: string
port: number
tls: boolean
}
function assertConfig(obj: unknown): asserts obj is Config {
if (typeof obj !== 'object' || obj === null) throw new Error('not an object')
if (!('host' in obj) || typeof (obj as any).host !== 'string') throw new Error('invalid host')
if (!('port' in obj) || typeof (obj as any).port !== 'number') throw new Error('invalid port')
if (!('tls' in obj) || typeof (obj as any).tls !== 'boolean') throw new Error('invalid tls')
}
function loadConfig(data: unknown) {
assertConfig(data)
// data: Config
return { url: `https://${data.host}:${data.port}`, tls: data.tls }
}
断言函数和类型谓词的区别:
- 类型谓词返回 boolean,用于
if/else分支 - 断言函数抛出或不抛出,用于校验前置条件后继续执行
窄化边界:哪些结构会触发窄化
不是所有检查都会触发窄化。TypeScript 的控制流分析在以下几种结构里生效:
// 1. if/else 分支
if (typeof x === 'string') { /* 窄化到 string */ }
// 2. switch/case
switch (x.type) {
case 'a': /* 窄化到 TypeA */
}
// 3. 逻辑运算符
const val = typeof x === 'string' ? x : x.toString()
// 两个分支都窄化
// 4. 提前 return
function example(x: string | null) {
if (x === null) return
// x: string
}
// 5. 赋值(解构不总是触发窄化)
let y: string | number = 'hello'
y = 42
// y: number —— 赋值会更新变量的类型
但以下情况不会触发窄化:
// 解构到变量后,原始窄化不传递
interface Wrapper { value: string | number }
function bad({ value }: Wrapper) {
if (typeof value === 'string') {
// value: string —— 可以
}
}
// 但是如果 value 是非原始类型,解构后的属性窄化在早期版本可能失效
// 在较新的 TypeScript 中,解构的属性窄化是支持的
自定义守卫 vs 类型断言
守卫和断言有一个根本区别:守卫是安全的类型窄化,断言是覆盖编译器的类型声明。
// 断言(不推荐)
function process(obj: unknown) {
const data = obj as Config
// 编译器不检查 obj 是否真的是 Config
}
// 守卫(推荐)
function processSafe(obj: unknown) {
if (isConfig(obj)) {
// obj: Config —— 编译器在 if 块内确认
}
}
断言只是告诉编译器"我知道它是什么类型"。自定义守卫则让编译器参与到类型验证的流程里。
总结
| 守卫类型 | 适用场景 | 安全性 |
|---|---|---|
typeof | Primitive 类型区分 | 编译器保证 |
instanceof | 类实例检查 | 编译器保证 |
in | 属性存在检查 | 编译器保证(有边界) |
类型谓词 x is T | 自定义条件窄化 | 开发者保证 |
断言签名 asserts x is T | 前置条件验证 | 开发者保证 |
类型守卫的演进路径是:从编译器内置的硬编码规则(typeof、instanceof),到开发者可定制的类型谓词,再到可以中断执行流的断言签名。越往后的守卫越灵活,但越需要开发者保证正确性——编译器信任了你的实现。


