Skip to content

我们已经了解,联合类型 (|) 为 TypeScript 带来了强大的灵活性,允许一个变量拥有多种可能性。但这种灵活性也带来了一个挑战:当我们持有一个联合类型的值时,如何安全地访问其特定类型的属性或方法?

直接访问是行不通的,因为 TypeScript 编译器无法保证在运行时变量的具体类型,为了防止错误,它只会允许我们访问所有联合成员的共有属性。要突破这一限制,我们就需要一种机制来向编译器“证明”在某个代码块中,变量是某个具体的类型。这个机制,就是类型守卫 (Type Guards)

类型守卫本质上是一个返回布尔值的运行时检查,但它的特殊之处在于,TypeScript 能够理解这些检查,并在它们返回 true 的代码块内,智能地收窄 (narrow) 变量的类型。让我们来深入探索几种最核心的类型守卫。

1. typeof 守卫:原始类型的辨识专家

当处理的联合类型包含 stringnumberbooleansymbolbigintundefinedfunction 等原始类型时,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 守卫便派上了用场。它通过检查对象的原型链来判断其“血统”。

让我们设想一个媒体播放器应用,它可以处理 SongAlbum 两种对象:

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. 自定义类型守卫:打造你自己的类型验证器

typeofinstanceof 功能强大,但它们无法处理基于接口(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 响应)和复杂的内部状态时,不可或缺的安全保障。

不知道说啥了很无语了