TypeScript 类型守卫详解与实战(十九)

在 TypeScript 开发中,类型守卫(Type Guards)是一个非常重要的概念。通过类型守卫,我们可以在运行时动态地检查变量的具体类型,从而确保代码的安全性和可靠性。本文将详细介绍 TypeScript 中的几种常见类型守卫,并通过实际示例展示如何在项目中应用这些技术。

什么是类型守卫

类型守卫是 TypeScript 中的一种类型缩小机制。它允许我们在运行时通过特定的条件检查,让编译器能够准确推断出变量的具体类型。通过类型守卫,我们可以安全地访问联合类型变量中特定类型的属性和方法。

为什么需要类型守卫

在 TypeScript 中,一个变量可能被声明为多种类型的联合。当我们需要根据不同类型执行不同操作时,编译器无法自动判断当前的具体类型。类型守卫就是解决这个问题的关键机制。通过类型守卫,我们可以确保在特定条件下访问到正确的类型,从而避免运行时错误。

typeof 类型守卫

typeof 是最常用的类型守卫,主要用于检查原始类型(如 string、number、boolean 等)。它返回一个字符串,表示值的类型。

typeof 基本用法

function processValue(value: string | number): void {
  if (typeof value === "string") {
    console.log("字符串长度:", value.length);
  } else {
    console.log("数字翻倍:", value * 2);
  }
}

processValue("hello"); // 输出: 字符串长度: 5
processValue(42);     // 输出: 数字翻倍: 84

typeof 支持的类型

  • "string" - 字符串类型
  • "number" - 数字类型(包括 NaN 和 Infinity)
  • "boolean" - 布尔类型
  • "undefined" - 未定义类型
  • "object" - 对象类型(注意:数组和 null 也会被识别为 "object")
  • "function" - 函数类型

> 注意:typeof 对于数组和 null 都会返回 "object"。如果需要精确区分数组和对象,需要使用其他方式。

instanceof 类型守卫

instanceof 用于检查对象是否是某个类的实例。它通过检查对象的原型链来判断类型。

instanceof 基本用法

class Dog {
  bark(): void {
    console.log("汪汪汪!");
  }
}

class Cat {
  meow(): void {
    console.log("喵喵喵!");
  }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

makeSound(new Dog()); // 输出: 汪汪汪!
makeSound(new Cat()); // 输出: 喵喵喵!

> 说明:instanceof 检查的是对象的原型链,因此它只能用于类实例,不能用于接口和类型别名。

自定义类型守卫

当内置的 typeof 和 instanceof 无法满足需求时,可以创建自定义类型守卫函数。自定义类型守卫使用 value is Type 的返回类型语法。

自定义守卫函数

function isString(value: any): value is string {
  return typeof value === "string";
}

function isNumber(value: any): value is number {
  return typeof value === "number";
}

function isArray(value: any): value is any[] {
  return Array.isArray(value);
}

function processValue(value: string | number | any[]): void {
  if (isString(value)) {
    console.log("字符串转大写:", value.toUpperCase());
  } else if (isNumber(value)) {
    console.log("数字格式化:", value.toFixed(2));
  } else if (isArray(value)) {
    console.log("数组长度:", value.length);
  }
}

processValue("hello");       // 输出: 字符串转大写: HELLO
processValue(3.14159);      // 输出: 数字格式化: 3.14
processValue([1, 2, 3, 4, 5]); // 输出: 数组长度: 5

> 提示:自定义类型守卫的关键是返回类型 value is Type,这是 TypeScript 识别类型守卫的标志。

in 操作符类型守卫

in 操作符可以检查对象是否包含某个属性。在条件判断中使用 in,TypeScript 会自动缩小对象的类型范围。

in 操作符用法

interface A {
  a: string;
}

interface B {
  b: number;
}

function process(obj: A | B): void {
  if ("a" in obj) {
    console.log("A 的属性 a:", obj.a);
  } else {
    console.log("B 的属性 b:", obj.b);
  }
}

process({ a: "hello" }); // 输出: A 的属性 a: hello
process({ b: 42 });      // 输出: B 的属性 b: 42

可辨识联合与类型守卫

可辨识联合是一种强大的模式,它通过一个公共的“标识”属性来区分联合类型成员。结合 switch 语句或 if 判断,可以实现完整的类型守卫。

可辨识联合实现计算器

interface Circle {

  kind: "circle";
  radius: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
  }
}

const circle = { kind: "circle" as const, radius: 5 };
const rectangle = { kind: "rectangle" as const, width: 4, height: 6 };
const triangle = { kind: "triangle" as const, base: 3, height: 4 };

console.log("圆形面积:", getArea(circle).toFixed(2)); // 输出: 圆形面积: 78.54
console.log("矩形面积:", getArea(rectangle));          // 输出: 矩形面积: 24
console.log("三角形面积:", getArea(triangle));         // 输出: 三角形面积: 6

> 说明:可辨识联合是 TypeScript 中最推荐使用的模式之一。它通过一个公共的字面量属性(通常是 kind 或 type)来区分不同的类型成员,使代码既类型安全又易于维护。

null 和 undefined 检查

处理可能为 null 或 undefined 的值时,直接的相等性检查也是有效的类型守卫。

null 检查

function getLength(str: string | null): number {
  if (str !== null) {
    return str.length;
  }
  return 0;
}

console.log(getLength("hello")); // 输出: 5
console.log(getLength(null));    // 输出: 0

> 提示:在启用 strictNullChecks 后,建议始终进行 null 检查。可以使用可选链(?.)和空值合并(??)来简化代码。

真值缩小

除了显式的类型检查,TypeScript 还会通过真值断言(Truthiness)来缩小类型范围。

真值缩小

function greet(name?: string): string {
  return name && "Hello, " + name;
}

console.log(greet("RUNOOB")); // 输出: Hello, RUNOOB
console.log(greet());         // 输出: Hello, undefined

注意事项

  • 类型守卫必须在条件分支中使用:只有在使用类型守卫进行条件判断后,TypeScript 才会进行类型缩小。
  • 返回类型必须是类型谓词:自定义类型守卫的返回类型必须是 value is Type 格式。
  • 可辨识联合是最佳实践:对于复杂的联合类型,建议使用可辨识联合模式。
  • 注意类型收窄的完整性:使用 switch 语句时,建议处理所有可能的分支。

> 建议:在处理联合类型时,优先考虑使用可辨识联合模式。它不仅代码更清晰,还能充分利用 TypeScript 的类型推断能力。

总结

类型守卫是 TypeScript 类型系统的重要组成部分,可以帮助我们在运行时动态地检查变量的具体类型,从而确保代码的安全性和可靠性。常见的类型守卫包括:

  • typeof: 最常用的方式,适用于原始类型的检查。
  • instanceof: 检查对象是否是特定类的实例。
  • 自定义守卫: 通过 value is Type 语法实现灵活的类检查。
  • in: 检查对象是否包含特定属性。
  • 可辨识联合: 推荐的最佳实践模式,通过标识字段区分类型。
  • 真值缩小: 利用 JavaScript 的真值判断进行类型收窄。

希望本文能帮助你更好地理解和应用 TypeScript 中的类型守卫,提升代码的质量和安全性。