我们已经了解,联合类型 (|) 为 TypeScript 带来了强大的灵活性,允许一个变量拥有多种可能性。但这种灵活性也带来了一个挑战:当我们持有一个联合类型的值时,如何安全地访问其特定类型的属性或方法?
直接访问是行不通的,因为 TypeScript 编译器无法保证在运行时变量的具体类型,为了防止错误,它只会允许我们访问所有联合成员的共有属性。要突破这一限制,我们就需要一种机制来向编译器“证明”在某个代码块中,变量是某个具体的类型。这个机制,就是类型守卫 (Type Guards)。
类型守卫本质上是一个返回布尔值的运行时检查,但它的特殊之处在于,TypeScript 能够理解这些检查,并在它们返回 true 的代码块内,智能地收窄 (narrow) 变量的类型。让我们来深入探索几种最核心的类型守卫。
1. typeof 守卫:原始类型的辨识专家
当处理的联合类型包含 string、number、boolean、symbol、bigint、undefined 或 function 等原始类型时,typeof 操作符是最直接、最有效的类型守卫。
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 两种对象:
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 类型。”
让我们来看一个经典的动物例子:
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 响应)和复杂的内部状态时,不可或缺的安全保障。
