跳转至

TypeScript 联合类型详解

联合类型是 TypeScript 中一个强大的类型系统特性,它允许一个值可以是多种类型中的一种。联合类型提供了类型安全的灵活性,是处理不确定类型场景的重要工具。

1. 联合类型概述

1.1 什么是联合类型?

联合类型表示一个值可以是多种类型中的一种,使用竖线 | 分隔各个类型。

1
2
3
4
5
6
// 变量可以是 string 或 number 类型
let value: string | number;

value = "Hello";    // 正确:string 类型
value = 42;         // 正确:number 类型
value = true;       // 错误:boolean 类型不在联合类型中

1.2 联合类型的优势

  • 类型安全:限制变量只能是指定类型之一
  • 灵活性:处理多种可能的输入类型
  • 可读性:明确表达变量的可能类型
  • 重构友好:类型系统帮助发现潜在问题

2. 基本用法

2.1 变量声明

// 基本类型联合
let id: string | number;
id = "user-123";  // OK
id = 123;         // OK
// id = true;     // Error: Type 'boolean' is not assignable to type 'string | number'

// 多个类型联合
let status: "success" | "error" | "loading";
status = "success";  // OK
status = "error";    // OK
// status = "failed"; // Error: Type '"failed"' is not assignable

// 对象类型联合
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

let shape: Shape = { kind: "circle", radius: 5 };  // OK

2.2 函数参数

// 函数接受联合类型参数
function formatValue(value: string | number): string {
  return `Value: ${value}`;
}

formatValue("Hello");  // "Value: Hello"
formatValue(42);       // "Value: 42"
// formatValue(true);  // Error: Argument of type 'boolean' is not assignable

// 返回联合类型
function parseInput(input: string): number | null {
  const num = Number(input);
  return isNaN(num) ? null : num;
}

const result1 = parseInput("42");    // number | null
const result2 = parseInput("hello"); // number | null

2.3 数组中的联合类型

// 数组元素可以是多种类型
let mixedArray: (string | number)[] = ["Hello", 42, "World", 100];

// 只读数组
const readonlyMixed: readonly (string | number)[] = ["a", 1, "b", 2];

// 元组中的联合类型
let tuple: [string, number | boolean, string?];
tuple = ["id", 123, "optional"];     // OK
tuple = ["id", true];                // OK (第三个元素可选)
// tuple = [123, "id"];              // Error: 位置不匹配

3. 类型守卫与类型收窄

3.1 typeof 类型守卫

function processValue(value: string | number) {
  // 使用 typeof 收窄类型
  if (typeof value === "string") {
    // 这里 value 的类型被收窄为 string
    console.log(value.toUpperCase());
  } else {
    // 这里 value 的类型被收窄为 number
    console.log(value.toFixed(2));
  }
}

// 更复杂的例子
function format(input: string | number | boolean): string {
  if (typeof input === "string") {
    return `String: ${input}`;
  } else if (typeof input === "number") {
    return `Number: ${input.toFixed(2)}`;
  } else {
    return `Boolean: ${input}`;
  }
}

3.2 instanceof 类型守卫

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function handlePet(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark();  // OK: pet 被收窄为 Dog
  } else {
    pet.meow();  // OK: pet 被收窄为 Cat
  }
}

const myDog = new Dog();
const myCat = new Cat();

handlePet(myDog);  // "Woof!"
handlePet(myCat);  // "Meow!"

3.3 自定义类型守卫

// 用户定义的类型守卫
function isString(value: any): value is string {
  return typeof value === "string";
}

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

function process(input: string | number) {
  if (isString(input)) {
    console.log(`String length: ${input.length}`);
  } else if (isNumber(input)) {
    console.log(`Number squared: ${input * input}`);
  }
}

// 更复杂的自定义类型守卫
interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin {
  id: number;
  name: string;
  permissions: string[];
}

function isAdmin(user: User | Admin): user is Admin {
  return "permissions" in user;
}

function getUserInfo(user: User | Admin) {
  if (isAdmin(user)) {
    console.log(`Admin permissions: ${user.permissions.join(", ")}`);
  } else {
    console.log(`User email: ${user.email}`);
  }
}

3.4 判别式联合

// 使用字面量类型作为判别属性
type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  side: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Shape = Circle | Square | Rectangle;

function getArea(shape: Shape): number {
  // 使用 switch 语句进行类型收窄
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    case "rectangle":
      return shape.width * shape.height;
    default:
      // 确保处理了所有情况
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

// 使用示例
const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", side: 4 };
const rectangle: Rectangle = { kind: "rectangle", width: 3, height: 6 };

console.log(getArea(circle));     // 78.53981633974483
console.log(getArea(square));     // 16
console.log(getArea(rectangle));  // 18

4. 联合类型的高级用法

4.1 与交叉类型结合

// 联合类型与交叉类型
type StringOrNumber = string | number;
type BooleanOrNull = boolean | null;

// 交叉类型:必须同时满足所有类型
type ComplexType = (StringOrNumber) & { metadata: any };

// 实际使用
const obj1: ComplexType = {
  metadata: { created: new Date() }
}; // Error: 缺少 StringOrNumber 类型

const obj2: ComplexType = {
  value: "hello",
  metadata: { created: new Date() }
} as any; // 需要类型断言

// 更实用的例子
type WithId = { id: string | number };
type WithName = { name: string };
type WithEmail = { email: string };

type User = WithId & (WithName | WithEmail);

const user1: User = { id: 1, name: "Alice" };           // OK
const user2: User = { id: "user-2", email: "bob@test.com" }; // OK
// const user3: User = { id: 3 };                       // Error: 缺少 name 或 email

4.2 条件类型中的联合类型

// 条件类型
type ExtractString<T> = T extends string ? T : never;

type Result1 = ExtractString<string | number | boolean>;  // string
type Result2 = ExtractString<number | boolean>;           // never

// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never;

type StringArray = ToArray<string>;           // string[]
type UnionArray = ToArray<string | number>;   // string[] | number[]

// 非分布式版本
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type NonDistArray = ToArrayNonDist<string | number>;  // (string | number)[]

4.3 映射类型中的联合类型

// 使用联合类型作为键
type UserRoles = "admin" | "editor" | "viewer";

type Permissions = {
  [K in UserRoles]: boolean;
};

const permissions: Permissions = {
  admin: true,
  editor: true,
  viewer: false
};

// 更复杂的映射
type EventTypes = "click" | "hover" | "focus";

type EventHandlers = {
  [K in EventTypes]: (event: Event) => void;
};

const handlers: EventHandlers = {
  click: (e) => console.log("clicked"),
  hover: (e) => console.log("hovered"),
  focus: (e) => console.log("focused")
};

5. 实用工具类型

5.1 内置工具类型

// Extract: 从联合类型中提取指定类型
type T0 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T1 = Extract<string | number | (() => void), Function>;  // () => void

// Exclude: 从联合类型中排除指定类型
type T2 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T3 = Exclude<string | number | (() => void), Function>;  // string | number

// NonNullable: 排除 null 和 undefined
type T4 = NonNullable<string | number | null | undefined>;  // string | number

// 实际应用
type FormField = {
  type: "text" | "number" | "checkbox";
  value: string | number | boolean;
  required?: boolean;
};

// 提取特定类型的字段
type TextField = Extract<FormField, { type: "text" }>;
type NumberField = Extract<FormField, { type: "number" }>;

// 排除特定类型的字段
type NonTextFields = Exclude<FormField, { type: "text" }>;

5.2 自定义工具类型

// 获取联合类型的所有可能值
type ValueOf<T> = T[keyof T];

interface Config {
  api: {
    endpoint: string;
    timeout: number;
  };
  ui: {
    theme: "light" | "dark";
    language: string;
  };
}

type ConfigValues = ValueOf<Config>;
// 等价于 { endpoint: string; timeout: number } | { theme: "light" | "dark"; language: string }

// 联合类型转元组
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type UnionToTuple<T> = UnionToIntersection<
  T extends any ? (t: T) => T : never
> extends (_: any) => infer W
  ? [...UnionToTuple<Exclude<T, W>>, W]
  : [];

type Test = UnionToTuple<"a" | "b" | "c">;  // ["a", "b", "c"]

6. 实际应用场景

6.1 API 响应处理

// API 响应类型
type ApiResponse<T> = {
  success: true;
  data: T;
  timestamp: Date;
} | {
  success: false;
  error: string;
  code: number;
  timestamp: Date;
};

// 处理函数
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return {
        success: false,
        error: `HTTP ${response.status}`,
        code: response.status,
        timestamp: new Date()
      };
    }

    const data = await response.json();
    return {
      success: true,
      data,
      timestamp: new Date()
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : "Unknown error",
      code: 500,
      timestamp: new Date()
    };
  }
}

// 使用示例
type UserData = { id: number; name: string; email: string };

async function getUser() {
  const response = await fetchData<UserData>("/api/user/1");

  if (response.success) {
    // 类型收窄:response 现在是成功类型
    console.log(`User: ${response.data.name}`);
    console.log(`Email: ${response.data.email}`);
  } else {
    // 类型收窄:response 现在是错误类型
    console.error(`Error ${response.code}: ${response.error}`);
  }

  return response;
}

6.2 表单验证

// 验证结果类型
type ValidationResult = 
  | { valid: true; value: string }
  | { valid: false; error: string; field: string };

// 验证函数
function validateEmail(email: string): ValidationResult {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (!email) {
    return { valid: false, error: "Email is required", field: "email" };
  }

  if (!emailRegex.test(email)) {
    return { valid: false, error: "Invalid email format", field: "email" };
  }

  return { valid: true, value: email };
}

function validatePassword(password: string): ValidationResult {
  if (!password) {
    return { valid: false, error: "Password is required", field: "password" };
  }

  if (password.length < 8) {
    return { valid: false, error: "Password must be at least 8 characters", field: "password" };
  }

  return { valid: true, value: password };
}

// 表单处理
function handleSubmit(email: string, password: string) {
  const emailResult = validateEmail(email);
  const passwordResult = validatePassword(password);

  // 检查所有验证结果
  const results = [emailResult, passwordResult];
  const errors = results.filter((r): r is Extract<ValidationResult, { valid: false }> => !r.valid);

  if (errors.length > 0) {
    console.error("Validation errors:", errors);
    return;
  }

  // 所有验证通过,可以安全地访问 value
  const validResults = results as Extract<ValidationResult, { valid: true }>[];
  console.log("Form submitted successfully");
  console.log("Email:", validResults[0].value);
  console.log("Password:", validResults[1].value);
}

6.3 状态管理

// 应用状态类型
type AppState = 
  | { status: "idle" }
  | { status: "loading"; requestId: string }
  | { status: "success"; data: any; timestamp: Date }
  | { status: "error"; error: string; retryCount: number };

// 状态转换函数
function handleStateTransition(current: AppState, action: any): AppState {
  switch (current.status) {
    case "idle":
      if (action.type === "FETCH_REQUEST") {
        return {
          status: "loading",
          requestId: action.requestId
        };
      }
      return current;

    case "loading":
      if (action.type === "FETCH_SUCCESS") {
        return {
          status: "success",
          data: action.data,
          timestamp: new Date()
        };
      }
      if (action.type === "FETCH_ERROR") {
        return {
          status: "error",
          error: action.error,
          retryCount: 0
        };
      }
      return current;

    case "success":
      if (action.type === "RESET") {
        return { status: "idle" };
      }
      return current;

    case "error":
      if (action.type === "RETRY") {
        return {
          status: "loading",
          requestId: `retry-${current.retryCount + 1}`
        };
      }
      return current;

    default:
      // 确保处理了所有状态
      const exhaustiveCheck: never = current;
      return exhaustiveCheck;
  }
}

// 使用示例
let state: AppState = { status: "idle" };

// 状态转换
state = handleStateTransition(state, { type: "FETCH_REQUEST", requestId: "req-1" });
console.log(state); // { status: "loading", requestId: "req-1" }

state = handleStateTransition(state, { type: "FETCH_SUCCESS", data: { user: "Alice" } });
console.log(state); // { status: "success", data: { user: "Alice" }, timestamp: Date }

7. 最佳实践与注意事项

7.1 避免过度使用联合类型

1
2
3
4
5
6
7
// 不好的实践:过度使用联合类型
type OverusedUnion = string | number | boolean | null | undefined | object | any[];

// 好的实践:使用更具体的类型
type ValidInput = string | number;
type Maybe<T> = T | null | undefined;
type ApiResponse<T> = { success: true; data: T } | { success: false; error: string };

7.2 优先使用判别式联合

// 不好的实践:没有判别属性
type Shape = 
  | { radius: number } 
  | { side: number } 
  | { width: number; height: number };

// 好的实践:使用判别属性
type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rectangle"; width: number; height: number };

7.3 合理使用类型守卫

// 不好的实践:过度使用类型断言
function process(value: string | number) {
  if ((value as any).toUpperCase) {
    console.log((value as string).toUpperCase());
  }
}

// 好的实践:使用类型守卫
function process(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // 自动类型收窄
  }
}

7.4 考虑性能影响

1
2
3
4
5
// 注意:大型联合类型可能影响编译性能
// 如果遇到性能问题,考虑:
// 1. 拆分大型联合类型
// 2. 使用接口继承
// 3. 启用增量编译

8. 常见错误与解决方案

8.1 无法访问公共属性

type Circle = { radius: number; color: string };
type Square = { side: number; color: string };

function getColor(shape: Circle | Square) {
  // 错误:Property 'color' does not exist on type 'Circle | Square'
  // return shape.color;

  // 解决方案:使用类型守卫
  if ("radius" in shape) {
    return shape.color; // OK: shape 被收窄为 Circle
  } else {
    return shape.color; // OK: shape 被收窄为 Square
  }
}

8.2 函数重载与联合类型

// 使用函数重载处理联合类型
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else {
    return input * 2;
  }
}

// 使用泛型约束
function process<T extends string | number>(input: T): T {
  // 需要类型守卫
  if (typeof input === "string") {
    return input.toUpperCase() as T;
  } else {
    return (input * 2) as T;
  }
}

8.3 数组过滤问题

const mixed: (string | number)[] = ["a", 1, "b", 2];

// 错误:filter 不会改变类型
const strings = mixed.filter(x => typeof x === "string");
// strings 的类型仍然是 (string | number)[]

// 解决方案:使用类型谓词
function isString(x: any): x is string {
  return typeof x === "string";
}

const strings2 = mixed.filter(isString);
// strings2 的类型是 string[]

9. 总结

联合类型是 TypeScript 类型系统的核心特性之一,它提供了处理多种可能类型的类型安全方式。通过合理使用联合类型,可以:

9.1 主要优势

  1. 类型安全:确保值只能是指定类型之一
  2. 表达力强:清晰表达变量的可能类型
  3. 灵活性高:适应多种业务场景
  4. 工具友好:IDE 提供更好的智能提示

9.2 关键概念

  1. 类型收窄:使用类型守卫缩小联合类型的范围
  2. 判别式联合:使用字面量类型区分联合类型成员
  3. 工具类型:利用 ExtractExclude 等工具类型操作联合类型
  4. 条件类型:在类型级别操作联合类型

9.3 学习建议

  1. 从简单开始:先掌握基本用法,再学习高级特性
  2. 实践为主:在实际项目中应用联合类型
  3. 阅读源码:学习优秀开源项目中的联合类型使用
  4. 关注更新:TypeScript 不断改进联合类型相关特性

9.4 进一步学习