类型守卫与收窄
TypeScript 最实用的特性之一:当联合类型的变量经过特定检查后,编译器能自动将类型收窄(narrowing)到更精确的子类型。本章覆盖 typeof、instanceof、in、真值收窄、自定义类型谓词、可辨识联合以及断言函数等全部守卫手段。
控制流分析(Control Flow Analysis)
TypeScript 会根据代码的执行路径自动推导出更精确的类型——这就是控制流分析。if/else、switch、return、throw 等语句都会影响类型收窄的结果。
function format(value: string | number): string {
// value 在此为 string | number
if (typeof value === "string") {
// value 在此被收窄为 string
return value.toUpperCase()
}
// value 在此被收窄为 number(由排除法得出)
return value.toFixed(2)
}🔬 深入原理:TypeScript 的控制流分析基于代码的静态结构——它不执行代码,而是分析每条可能的执行路径上变量的类型。在
if块内部,TS 从原联合类型中排除不满足条件的成员;在else或if之后的代码中,已经检查过的分支被排除。
typeof 守卫
typeof 是最常用的类型守卫,TS 能识别以下返回值:"string"、"number"、"bigint"、"boolean"、"symbol"、"undefined"、"object"、"function"。
function process(value: string | number | boolean): string {
if (typeof value === "string") return value
if (typeof value === "number") return value.toString()
if (typeof value === "boolean") return value ? "yes" : "no"
// value 在此为 never(穷尽检查)
return value // ✅ never
}🚨 陷阱:
typeof null === "object",但 TS 不会把null收窄为object类型。typeof守卫对null、数组、普通对象等无法做精细化区分——这些场景需用后续介绍的instanceof、in或自定义类型谓词。
instanceof 守卫
instanceof 用于判断一个对象是否为某个类的实例,TS 会据此收窄类型。适用于基于 ES6 class 构建的类型体系。
class Dog { bark() { return "woof" } }
class Cat { meow() { return "meow" } }
function speak(animal: Dog | Cat): string {
if (animal instanceof Dog) return animal.bark()
if (animal instanceof Cat) return animal.meow()
// 穷尽性检查
const _: never = animal
return _
}💡 最佳实践:函数末尾的
const _: never = animal是一个编译时穷尽性检查——如果日后新增类型成员且忘记处理,TS 会在这里报错。
in 操作符收窄
in 操作符检查某个属性是否存在于对象上,TS 据此将联合类型中的 interface 收窄到正确的分支。
interface Fish { swim(): void }
interface Bird { fly(): void }
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim() // animal: Fish
} else {
animal.fly() // animal: Bird
}
}真值收窄(Truthiness Narrowing)
在条件表达式 if (value) 中,TS 会自动过滤掉 falsy 值对应的类型。
function printAll(strs: string | null | undefined): void {
if (strs) {
// strs: string(null 和 undefined 均为 falsy,已被排除)
console.log(strs.toUpperCase())
}
}🚨 陷阱:空字符串
""、0、NaN都是 falsy 值——真值检查可能意外过滤掉有效数据。需要精确区分null/undefined时,请使用strs != null(同时排除null和undefined)或显式比较strs !== null && strs !== undefined。
相等收窄(Equality Narrowing)
利用 ===、!==、==、!= 等比较操作也能触发类型收窄。当两个变量被比较且它们有共同的类型时,TS 会同时收窄双方。
function example(x: string | number, y: string | boolean): void {
if (x === y) {
// x 和 y 在此均被收窄为 string(唯一共同类型)
console.log(x.toUpperCase(), y.toUpperCase())
}
}!= null 是一个非常实用的收窄技巧,能同时排除 null 和 undefined:
function greet(name: string | null | undefined): string {
if (name != null) {
// name: string
return `Hello, ${name}`
}
return "Hello, stranger"
}自定义类型谓词(is)
类型谓词(Type Predicate)让你封装复杂的类型判断逻辑,并在任何地方复用。返回值类型写成 parameter is Type 的形式。
// 类型谓词:返回值类型为 "value is string"
function isString(value: unknown): value is string {
return typeof value === "string"
}
function process(input: unknown): string {
if (isString(input)) {
// input 在此被收窄为 string!
return input.toUpperCase()
}
return String(input)
}复杂场景——接口层级收窄
interface User { name: string; email: string }
interface Admin extends User { role: "admin"; permissions: string[] }
function isAdmin(user: User): user is Admin {
return "role" in user && user.role === "admin"
}配合数组 filter 使用
const items: (string | null)[] = ["a", null, "b", null, "c"]
// 使用 is 类型谓词的 filter 回调会自动收窄结果数组类型
const validItems: string[] = items.filter(
(item): item is string => item !== null
)💡 最佳实践:类型谓词让你可以将复杂的判断逻辑封装为可复用的函数。在 API 响应类型收窄、表单验证、数据清洗(如
filter过滤null)等场景中非常实用。配合unknown参数更能构建类型安全的边界层。
可辨识联合(Discriminated Union)
这是 TypeScript 最强大的模式之一:给联合类型的每个成员添加一个共同的字面量判别字段(discriminant property,通常命名为 kind 或 type),TS 能根据该字段的值精确收窄类型。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number }
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2
case "square": return shape.side ** 2
case "rectangle": return shape.width * shape.height
}
}可辨识联合同样适用于异步状态管理(Redux Actions、API 请求状态等):
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error }
function renderState<T>(state: RequestState<T>): string {
switch (state.status) {
case "idle": return "——"
case "loading": return "加载中……"
case "success": return `结果:${state.data}`
case "error": return `错误:${state.error.message}`
}
}🔬 深入原理:可辨识联合 = 联合类型 + 字面量判别字段。TS 在对
switch (shape.kind)进行分支判断时,会将每个case分支内的shape类型收窄到对应成员,因此可以直接访问该成员独有的属性(如shape.radius)而无需类型断言。
never 穷尽性检查
借助 never 类型和 assertNever 辅助函数,可以在编译期确保已覆盖联合类型的所有成员。新增成员时如果忘记更新处理逻辑,TS 会直接报错。
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`)
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2
case "square": return shape.side ** 2
case "rectangle": return shape.width * shape.height
default:
return assertNever(shape)
// ✅ 编译期保证:如果 Shape 新增成员而此处未处理,TS 会报错
}
}🚨 陷阱:没有
assertNever检查时,新增联合成员后代码会默默执行default分支(通常返回undefined或静默失败),不会产生任何编译警告。将assertNever放在default中是最小成本、最大收益的防御性编程手段。
断言函数(asserts)
断言函数使用 asserts 返回类型标注,在运行时抛出异常,在编译时收窄类型。与类型谓词不同,断言函数不返回布尔值——如果函数正常返回(不抛异常),说明断言通过,TS 即收窄后续代码路径中的类型。
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) throw new Error(message || "Assertion failed")
}
function getLength(obj: unknown): number {
assert(typeof obj === "string" || Array.isArray(obj))
// assert 正常返回后,obj 被收窄为 string | any[]
return obj.length // ✅ OK
}类型谓词 vs 断言函数
| 特性 | 类型谓词 value is Type |
断言函数 asserts value |
|---|---|---|
| 返回值 | boolean,调用方自行判断 |
无返回值,不通过时抛异常 |
| 收窄方式 | 仅在 if 块内收窄 |
正常返回后,后续代码全部收窄 |
| 典型场景 | filter、条件分支 | 参数校验、前置条件检查 |
| 错误处理 | 调用方决定 | 函数内部抛出 |
// 类型谓词:调用方需 if 判断
function isString(v: unknown): v is string {
return typeof v === "string"
}
// 断言函数:不满足则抛异常,满足则自动收窄
function assertString(v: unknown): asserts v is string {
if (typeof v !== "string") throw new TypeError("Expected string")
}
function demo(input: unknown): string {
assertString(input)
// input 在此已被收窄为 string——无需 if 判断!
return input.toUpperCase()
}💡 最佳实践:断言函数适合在函数入口处做参数校验,让剩余代码在安全收窄后的类型上运作。这在处理
unknown输入、JSON 解析结果等外部数据边界时格外有用。
常见陷阱汇总
| 陷阱 | 说明 | 解决 |
|---|---|---|
typeof null === "object" |
TS 不会将 null 收窄为 object,且 typeof 无法区分数组和普通对象 |
使用 Array.isArray()、instanceof 或自定义类型谓词 |
真值检查误伤 ""、0、NaN |
if (value) 会把这些有效值当作 falsy 过滤掉 |
精确比较:value !== null && value !== undefined 或 value != null |
in 操作符无法收窄原始类型 |
"length" in value 对 string 和数组都返回 true |
在 interface 层面设计足够区分彼此的属性名 |
| 类型谓词写错不报错 | user is Admin 逻辑写错时 TS 不会提示,会将错误类型带到后续代码 |
类型谓词逻辑务必加上单元测试 |
| 忘记穷尽性检查 | 新加联合成员时 default 分支默默执行,无编译报错 | 始终在 default 分支调用 assertNever() |