Skip to content
类型守卫与收窄

类型守卫与收窄

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 从原联合类型中排除不满足条件的成员;在 elseif 之后的代码中,已经检查过的分支被排除。


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、数组、普通对象等无法做精细化区分——这些场景需用后续介绍的 instanceofin 或自定义类型谓词。


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())
    }
}

🚨 陷阱:空字符串 ""0NaN 都是 falsy 值——真值检查可能意外过滤掉有效数据。需要精确区分 null/undefined 时,请使用 strs != null(同时排除 nullundefined)或显式比较 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 是一个非常实用的收窄技巧,能同时排除 nullundefined

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,通常命名为 kindtype),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 或自定义类型谓词
真值检查误伤 ""0NaN if (value) 会把这些有效值当作 falsy 过滤掉 精确比较:value !== null && value !== undefinedvalue != null
in 操作符无法收窄原始类型 "length" in valuestring 和数组都返回 true 在 interface 层面设计足够区分彼此的属性名
类型谓词写错不报错 user is Admin 逻辑写错时 TS 不会提示,会将错误类型带到后续代码 类型谓词逻辑务必加上单元测试
忘记穷尽性检查 新加联合成员时 default 分支默默执行,无编译报错 始终在 default 分支调用 assertNever()