15.类型守卫 (Type Guards) 副本.md 来源:https://my.feishu.cn/file/TjYdbxKOmo1a7CxgE7bcEFqgn2g 采集状态:已完成正文采集;已从查看代码模式逐行提取,行号 1 至 119 连续,并核对滚动到底。 我们已经了解,联合类型 (`|`) 为 TypeScript 带来了强大的灵活性,允许一个变量拥有多种可能性。但这种灵活性也带来了一个挑战:当我们持有一个联合类型的值时,如何安全地访问其特定类型的属性或方法? 直接访问是行不通的,因为 TypeScript 编译器无法保证在运行时变量的具体类型,为了防止错误,它只会允许我们访问所有联合成员的**共有**属性。要突破这一限制,我们就需要一种机制来向编译器“证明”在某个代码块中,变量是某个具体的类型。这个机制,就是**类型守卫 (Type Guards)**。 类型守卫本质上是一个返回布尔值的运行时检查,但它的特殊之处在于,TypeScript 能够理解这些检查,并在它们返回 `true` 的代码块内,智能地**收窄 (narrow)** 变量的类型。让我们来深入探索几种最核心的类型守卫。 ### 1. `typeof` 守卫:原始类型的辨识专家 当处理的联合类型包含 `string`、`number`、`boolean`、`symbol`、`bigint`、`undefined` 或 `function` 等原始类型时,`typeof` 操作符是最直接、最有效的类型守卫。 ```typescript function processInput(input: string | number | string[]) { if (typeof input === "string") { // 在此代码块内,TypeScript 确信 input 是 string console.log(input.toUpperCase()); } else if (typeof input === "number") { // 在此代码块内,TypeScript 确信 input 是 number console.log(input.toFixed(2)); } else { // 在此代码块内,TypeScript 推断 input 是 string[] console.log(`Array length: ${input.length}`); } } ``` 值得注意的是,`typeof null` 在 JavaScript 中返回 `"object"`,这是一个历史遗留问题。因此,如果你的联合类型中包含 `null`,需要用 `input === null` 这样的相等性检查来单独处理。 ### 2. `instanceof` 守卫:类实例的身份检查器 `typeof` 对所有非函数对象的返回值都是 `"object"`,这使得它无法区分两个不同的类实例。当我们需要检查一个对象是否由某个特定的类创建时,`instanceof` 守卫便派上了用场。它通过检查对象的原型链来判断其“血统”。 让我们设想一个媒体播放器应用,它可以处理 `Song` 和 `Album` 两种对象: ```typescript class Song { constructor(public title: string, public duration: number) {} play() { console.log(`Playing song: ${this.title}`); } } class Album { constructor(public name: string, public songs: Song[]) {} listSongs() { console.log(`Songs in album ${this.name}:`); this.songs.forEach(song => console.log(`- ${song.title}`)); } } function playMedia(media: Song | Album) { if (media instanceof Song) { // 在此代码块内,TypeScript 知道 media 是 Song 的实例 media.play(); } else if (media instanceof Album) { // 在此代码块内,TypeScript 知道 media 是 Album 的实例 media.listSongs(); } } ``` `instanceof` 是处理面向对象编程中多态行为的理想工具,它清晰地划分了不同类实例的处理逻辑。 ### 3. 自定义类型守卫:打造你自己的类型验证器 `typeof` 和 `instanceof` 功能强大,但它们无法处理基于接口(`interface`)或纯对象字面量定义的类型。当我们需要根据对象的结构特征(比如是否存在某个属性)来区分类型时,就需要创建**自定义类型守卫**。 自定义类型守卫本质上是一个函数,其特殊之处在于它的返回值类型是一个**类型谓词 (Type Predicate)**,形如 `parameterName is Type`。 这个特殊的返回类型向 TypeScript 传达了一个明确的信号:“如果这个函数返回 `true`,那么你可以确信传入的参数就是 `Type` 类型。” 让我们来看一个经典的动物例子: ```typescript interface Fish { swim(): void; } interface Bird { fly(): void; } // 这就是一个自定义类型守卫函数 function isFish(pet: Fish | Bird): pet is Fish { // 我们通过检查 pet 是否有 swim 方法来判断它是不是 Fish // (pet as Fish).swim 是一种类型断言,确保 swim 属性可被访问 return (pet as Fish).swim !== undefined; } function move(pet: Fish | Bird) { if (isFish(pet)) { // 在此代码块内,TypeScript 因为 isFish 返回 true, // 所以确信 pet 的类型是 Fish pet.swim(); } else { // 否则,TypeScript 推断 pet 的类型是 Bird pet.fly(); } } ``` 通过 `pet is Fish` 这个返回类型,`isFish` 函数不仅仅返回了一个布尔值,它还直接参与到了 TypeScript 的类型流分析中,为我们编写的 `if` 语句赋予了类型收窄的能力。 ### 其他守卫技术 除了上述三种核心守卫,还有一些常用的模式也利用了类型收窄的原理: * **`in` 操作符**:通过检查属性是否存在来区分类型,例如 `if ("swim" in pet)`。 * **相等性检查**:当与字面量类型结合时,`===`, `==`, `!==`, `!=` 也能作为类型守卫。这正是**可辨识联合**模式的核心,通过检查一个共享的“标签”属性(如 `shape.kind === "circle"`)来实现最安全、最清晰的类型收窄。 ### 总结 类型守卫是 TypeScript 中连接动态世界与静态类型安全世界的桥梁。它们让我们能够在享受联合类型带来的灵活性的同时,通过在运行时进行必要的检查,来向编译器提供足够的信息,从而在特定的代码区域内恢复类型的精确性。 * 使用 **`typeof`** 来处理原始类型。 * 使用 **`instanceof`** 来处理类实例。 * 使用**自定义类型守卫** (`pet is Fish`) 来处理更复杂的对象结构和业务逻辑。 熟练掌握并运用这些类型守卫,是编写出健壮、可读且真正能发挥 TypeScript 优势的代码的关键。它们是你在处理不确定的外部数据(如 API 响应)和复杂的内部状态时,不可或缺的安全保障。