TypeScript 联合类型(长文讲解)

什么是 TypeScript 联合类型?

在 TypeScript 中,类型系统是它的核心优势之一。当我们面对一个变量可能有多种类型时,传统的静态类型语言会让我们陷入困境:要么定义太宽泛(比如 any),要么定义太严格(无法兼容多种情况)。这时,TypeScript 联合类型就登场了,它就像一个“多功能开关”,允许一个值同时属于多个类型中的一种。

想象一下,你家的电灯开关可以控制两种灯:白光灯和暖光灯。你不需要两个开关,只需要一个能切换模式的开关。联合类型就是 TypeScript 中的这种“智能开关”——它告诉你:“这个变量可以是 A 类型,也可以是 B 类型,但只能是其中之一。”

语法上,联合类型使用竖线 | 连接多个类型,例如:string | number | boolean 表示这个值可以是字符串、数字或布尔值中的任意一种。

这种设计让代码既保持了类型安全,又具备了足够的灵活性,特别适合处理 API 返回值、表单输入、配置项等复杂场景。


联合类型的常见使用场景

在实际开发中,联合类型几乎无处不在。我们来看看几个典型的应用场景。

处理 API 返回值

很多后端接口返回的数据结构是动态的。比如一个用户信息接口,可能返回用户对象,也可能返回错误信息。这时用联合类型就非常合适。

// 定义两种可能的返回结果类型
type UserResponse = {
  success: true;
  data: {
    id: number;
    name: string;
    email: string;
  };
};

type ErrorResponse = {
  success: false;
  message: string;
  code: number;
};

// 使用联合类型表示最终可能的结果
type ApiResponse = UserResponse | ErrorResponse;

// 示例函数
function fetchUser(userId: number): ApiResponse {
  // 模拟网络请求
  if (userId === 1) {
    return {
      success: true,
      data: {
        id: 1,
        name: "张三",
        email: "zhangsan@example.com"
      }
    };
  } else {
    return {
      success: false,
      message: "用户不存在",
      code: 404
    };
  }
}

// 使用示例
const result = fetchUser(1);

// 类型检查:TS 会知道 result 的类型是 UserResponse | ErrorResponse
if (result.success) {
  // TS 知道此时一定是 UserResponse 类型
  console.log("用户姓名:", result.data.name); // ✅ 安全访问 data
} else {
  // 此时一定是 ErrorResponse 类型
  console.log("错误码:", result.code); // ✅ 安全访问 code
}

注释说明:

  • ApiResponseUserResponseErrorResponse 的联合类型,表示结果可能是其中任意一种。
  • 通过 result.success 的判断,TypeScript 可以自动收窄类型(Narrowing),让后续访问属性更安全。
  • 这种模式在实际项目中非常常见,尤其在处理异步请求时。

表单字段的动态类型

在前端开发中,表单字段的值可能是字符串、数字,甚至是空值。联合类型能很好地应对这种情况。

// 表单字段可能的值类型
type FormValue = string | number | null;

// 定义一个表单数据结构
interface FormState {
  username: FormValue;
  age: FormValue;
  isActive: boolean;
}

// 示例初始化
const form: FormState = {
  username: "李四",
  age: 25,
  isActive: true
};

// 修改字段值
form.age = null; // ✅ 合法,null 属于 number | string | null
form.username = 300; // ❌ 报错!300 是数字,但 username 是 string | null,不能赋值数字

// 但我们可以这样赋值:
form.username = "新用户名"; // ✅ 正确
form.age = 28; // ✅ 正确

注释说明:

  • FormValue 使用联合类型,表示一个字段可以是字符串、数字或空值。
  • 这样既避免了使用 any 导致的类型不安全,又保留了灵活性。
  • 当我们尝试把数字赋给 username 时,TypeScript 会阻止错误行为,提醒我们类型不匹配。

类型收窄:让联合类型更智能

联合类型最强大的地方,不是定义多个类型,而是它能配合条件判断进行类型收窄(Type Narrowing)。这是 TypeScript 智能推理的关键所在。

使用 typeof 进行收窄

当联合类型包含基本类型时,可以用 typeof 来判断具体类型。

function processInput(value: string | number | boolean) {
  if (typeof value === "string") {
    // 此时 TypeScript 知道 value 是 string 类型
    console.log("输入的是字符串:", value.toUpperCase());
  } else if (typeof value === "number") {
    // 此时 value 是 number 类型
    console.log("输入的是数字:", value.toFixed(2));
  } else {
    // 此时 value 是 boolean 类型
    console.log("输入的是布尔值:", value ? "真" : "假");
  }
}

// 测试
processInput("hello");     // 字符串处理
processInput(3.14159);     // 数字处理
processInput(true);        // 布尔处理

注释说明:

  • typeof value === "string" 这个条件成立后,TypeScript 会自动将 value 的类型收窄为 string
  • 这种机制让代码既安全又高效,无需手动类型断言。

使用 in 操作符判断对象结构

当联合类型是对象时,可以用 in 操作符来判断某个属性是否存在。

type Animal = {
  name: string;
  species: string;
  legs: number;
};

type Bird = {
  name: string;
  wingspan: number;
  canFly: boolean;
};

type Pet = Animal | Bird;

function describePet(pet: Pet) {
  // 判断是否包含 wingspan 属性
  if ("wingspan" in pet) {
    // TS 知道 pet 是 Bird 类型
    console.log(`${pet.name} 是一只鸟,翼展为 ${pet.wingspan} 厘米`);
  } else {
    // TS 知道 pet 是 Animal 类型
    console.log(`${pet.name} 是一种动物,有 ${pet.legs} 条腿`);
  }
}

// 测试
const cat = { name: "小花", species: "猫", legs: 4 };
const eagle = { name: "老鹰", wingspan: 200, canFly: true };

describePet(cat);   // 输出:小花 是一种动物,有 4 条腿
describePet(eagle); // 输出:老鹰 是一只鸟,翼展为 200 厘米

注释说明:

  • in 操作符检查对象是否拥有某个属性。
  • 一旦判断成立,TypeScript 就能推断出更具体的类型,从而启用该类型独有的属性。

联合类型的高级技巧

与类型别名结合使用

为了提高可读性,推荐将复杂的联合类型用 type 别名封装。

// 定义常见状态类型
type LoadingState = "loading" | "loaded" | "error";

// 使用别名
function handleState(state: LoadingState) {
  switch (state) {
    case "loading":
      console.log("正在加载...");
      break;
    case "loaded":
      console.log("数据已加载完成");
      break;
    case "error":
      console.log("加载失败,请重试");
      break;
  }
}

// 使用
handleState("loading");   // ✅ 合法
handleState("unknown");   // ❌ 报错!类型不匹配

注释说明:

  • 使用 type 别名让联合类型更易管理,也便于复用。
  • 这种方式在状态管理、枚举类场景中非常实用。

never 类型配合

在某些情况下,联合类型可以包含 never,表示“不可能发生的情况”。

function handleError(error: string | number): never {
  if (typeof error === "string") {
    throw new Error(`错误信息:${error}`);
  } else {
    throw new Error(`错误码:${error}`);
  }
  // 两个分支都抛出异常,函数永远不会正常返回
}

// 使用
try {
  handleError("网络超时");
} catch (e) {
  console.error(e.message);
}

注释说明:

  • never 表示函数“永远不会返回”,常用于抛出异常或无限循环。
  • 联合类型中包含 never 时,TS 会自动忽略它,使类型更精确。

实际项目中的最佳实践

在真实项目中,合理使用 TypeScript 联合类型能极大提升代码质量。

  • 避免过度使用 any:当不确定类型时,优先考虑联合类型,而不是直接用 any
  • 保持类型清晰:联合类型虽然灵活,但不宜过长。如果超过 3 个类型,建议拆分为多个类型别名。
  • 善用类型收窄:配合 ifswitch 等条件语句,让 TypeScript 自动推断更具体的类型。
  • 结合工具类型:如 ExtractExclude 等,进一步过滤联合类型中的成员。

总结

TypeScript 联合类型是类型系统中极具实用价值的特性。它不仅解决了“一个变量多种类型”的痛点,还通过类型收窄机制让代码在保持类型安全的同时拥有足够的灵活性。

从 API 返回值、表单处理到状态管理,联合类型几乎贯穿了整个开发流程。掌握它,意味着你能写出更健壮、更易维护的 TypeScript 代码。

在实际项目中,不要害怕使用联合类型,只要遵循“清晰、可读、可维护”的原则,它就会成为你开发中的得力助手。