Skip to content

在编程的世界里,数据并非总是非黑即白、一成不变。我们经常会遇到这样的场景:一个函数的参数既可以是数字,也可以是字符串;一个变量可能保存着成功的结果,也可能是一个错误对象。在传统的 JavaScript 中,我们依赖于运行时检查来处理这种多样性,但这往往伴随着不确定性和潜在的错误。

TypeScript 为此提供了一个优雅而强大的解决方案——联合类型 (Union Types)。它允许我们声明一个值可以是多种类型中的任意一种,从而在保持灵活性的同时,享受静态类型检查带来的安全性。

什么是联合类型?

联合类型的概念非常直观,它使用 | (管道符) 作为分隔符,表示一个值可以是列出的类型之一。

其基本语法如下:

typescript
let myVar: string | number;

myVar = "Hello"; // 合法
myVar = 42;      // 合法

// myVar = true; // 编译时错误! Type 'boolean' is not assignable to type 'string | number'.

通过 string | number,我们向 TypeScript 声明:myVar 这个变量的“合法身份”既可以是 string,也可以是 number。任何其他类型的值都会被编译器拒绝。

一个非常经典的用例是处理可以由不同形式表示的 ID:

typescript
function findUserById(id: string | number) {
  // ...
}

findUserById(101);       // 合法
findUserById("user-abc"); // 合法

挑战:如何安全地使用联合类型?

联合类型带来了灵活性,但随之也产生了一个重要的问题:当一个值的类型可能是 stringnumber 时,我们能对它做什么操作?

TypeScript 在这里采取了非常严谨和安全的策略:你只能访问联合类型中所有成员都共有的属性或方法。

typescript
function processValue(value: string | number) {
  // console.log(value.toUpperCase()); 
  // 编译时错误: Property 'toUpperCase' does not exist on type 'string | number'.
  // Property 'toUpperCase' does not exist on type 'number'.

  // toString() 是 string 和 number 都共有的方法,所以是合法的。
  console.log(value.toString()); 
}

编译器之所以报错,是因为它无法保证 value 在运行时一定是 string 类型。如果 value 恰好是 number,调用 toUpperCase() 就会导致运行时错误。为了防患于未然,TypeScript 在编译阶段就禁止了这种不确定的操作。

解决方案:类型收窄 (Type Narrowing)

为了充分利用联合类型的能力,我们需要一种方法来确定在代码的某个特定分支中,变量到底是哪个具体的类型。这个过程,在 TypeScript 中被称为类型收窄 (Type Narrowing)

TypeScript 会通过分析我们的代码逻辑(如条件判断),智能地“收窄”变量的类型范围。以下是几种最常见的类型收窄技术:

1. typeof 类型守卫

当处理原始类型(如 string, number, boolean)的联合时,typeof 是最直接有效的工具。

typescript
function formatId(id: string | number): string {
  if (typeof id === "string") {
    // 在这个代码块内,TypeScript 确信 id 的类型是 string
    return id.toUpperCase();
  } else {
    // 在这个代码块内,TypeScript 确信 id 的类型是 number
    return `ID-${id.toFixed(2)}`;
  }
}

2. in 操作符守卫

当联合类型中包含对象时,我们可以使用 in 操作符来检查某个属性是否存在于对象上,从而区分不同的对象类型。

typescript
interface Admin {
  name: string;
  privileges: string[];
}

interface Customer {
  name: string;
  credit: number;
}

type User = Admin | Customer;

function displayUserInfo(user: User) {
  console.log(`Name: ${user.name}`);
  if ("privileges" in user) {
    // 在这里,user 的类型被收窄为 Admin
    console.log(`Privileges: ${user.privileges.join(", ")}`);
  }
  if ("credit" in user) {
    // 在这里,user 的类型被收窄为 Customer
    console.log(`Credit: ${user.credit}`);
  }
}

3. 字面量类型与可辨识联合 (Discriminated Unions)

这是处理联合类型最高级、也是最推荐的模式,尤其适用于状态管理等场景。可辨识联合要求联合中的每个对象类型都拥有一个共同的、值为字面量类型的属性(即“可辨识”或“标签”属性)。

我们通常使用 switch 语句来检查这个“标签”属性,TypeScript 会在每个 case 分支中完美地收窄类型。

typescript
interface Circle {
  kind: "circle"; // 辨识属性
  radius: number;
}

interface Square {
  kind: "square"; // 辨识属性
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // shape 的类型被收窄为 Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // shape 的类型被收窄为 Square
      return shape.sideLength ** 2;
    default:
      // 利用 never 类型进行穷尽性检查,确保所有 case 都被处理
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

可辨识联合模式不仅代码清晰,而且具有极高的类型安全性。如果未来我们为 Shape 添加了新的类型(如 Triangle),而没有在 getArea 函数中处理它,default 分支的 never 类型检查就会立刻引发编译错误,提醒我们更新代码。

总结

联合类型是 TypeScript 类型系统中不可或缺的一环,它为我们提供了一种在类型安全和代码灵活性之间取得完美平衡的方式。

  • 它使用 | 运算符,允许一个变量可以是多种类型之一。
  • 默认情况下,我们只能访问联合类型成员的共有属性
  • 通过类型收窄typeof, in, 可辨识联合等),我们可以在特定的代码分支中确定变量的具体类型,从而安全地执行特定类型的操作。

掌握联合类型及其收窄技术,意味着你能够以一种既灵活又健壮的方式来为真实世界的复杂数据建模,彻底告别 any 类型带来的不确定性。

不知道说啥了很无语了