强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

08 - 联合与交叉类型

联合与交叉类型

联合类型(Union Types)

联合类型表示一个值可以是多种类型之一,使用 | 分隔:

// 基本联合类型
let value: string | number;
value = "hello";  // ✅
value = 42;       // ✅
value = true;     // ❌ boolean 不在联合类型中

// 函数参数联合类型
function printId(id: string | number): void {
  console.log(`ID: ${id}`);
}

printId("abc"); // ✅
printId(123);   // ✅

联合类型的限制

function getLength(value: string | number): number {
  // ❌ 不能直接调用只属于某个类型的方法
  // return value.length; // number 没有 length

  // ✅ 需要先进行类型收窄
  if (typeof value === "string") {
    return value.length; // 这里 value 被收窄为 string
  }
  return value.toString().length;
}

字面量类型(Literal Types)

字面量类型是更具体的类型,限定变量只能是某个特定的值:

// 字符串字面量类型
type Direction = "up" | "down" | "left" | "right";

let dir: Direction = "up";   // ✅
dir = "forward";              // ❌ 不在字面量类型中

// 数字字面量类型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3;      // ✅
roll = 7;                     // ❌

// 布尔字面量类型
type True = true;
let flag: True = true;        // ✅
flag = false;                  // ❌

模板字面量类型(Template Literal Types)

// 基本模板字面量
type Greeting = `Hello, ${string}!`;
const msg1: Greeting = "Hello, World!"; // ✅
const msg2: Greeting = "Hi, World!";    // ❌

// 联合展开
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" | "blue-small" | ...

// 内置字符串操作
type UpperDirection = Uppercase<Direction>;   // "UP" | "DOWN" | ...
type LowerDirection = Lowercase<Direction>;   // "up" | "down" | ...
type CapDirection = Capitalize<Direction>;    // "Up" | "Down" | ...

as const 断言

// 普通声明会被推断为宽泛类型
const colors = ["red", "green", "blue"]; // string[]

// as const 使所有内容变为只读字面量类型
const COLORS = ["red", "green", "blue"] as const;
// 类型: readonly ["red", "green", "blue"]

// 用于对象
const config = {
  host: "localhost",
  port: 3000,
  debug: false
} as const;
// 类型: { readonly host: "localhost"; readonly port: 3000; readonly debug: false }

// 配合 typeof 获取字面量类型
type Config = typeof config;
// { readonly host: "localhost"; readonly port: 3000; readonly debug: false }

交叉类型(Intersection Types)

交叉类型将多个类型合并为一个类型,使用 & 分隔:

// 基本交叉类型
type Named = { name: string };
type Aged = { age: number };
type Employed = { company: string };

// 交叉类型:同时满足所有类型
type Person = Named & Aged & Employed;

const person: Person = {
  name: "Alice",
  age: 25,
  company: "Tech Corp"
};
// 三个属性都必须有,缺一不可

交叉类型 vs 接口继承

// 接口继承
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// 交叉类型(等价)
type AnimalType = { name: string };
type DogType = AnimalType & { breed: string };

// 两者效果相同,选择依据:
// - 接口继承:更清晰,支持声明合并
// - 交叉类型:更灵活,可用于联合类型组合

联合与交叉的组合

// 联合类型的交叉
type Result = (string | number) & (boolean | string);
// 等价于 string(两种类型都包含 string)

// 对象联合类型
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

// 判别联合(Discriminated Union)
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;
  }
}

可辨识联合(Discriminated Unions)

这是 TypeScript 中最实用的模式之一:

// 每个成员都有一个共同的可辨识属性(discriminant)
type ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string }
  | { status: "loading" };

function handleResponse<T>(response: ApiResponse<T>): void {
  switch (response.status) {
    case "success":
      console.log(response.data);   // ✅ T 类型
      break;
    case "error":
      console.log(response.error);  // ✅ string 类型
      break;
    case "loading":
      console.log("加载中...");
      break;
  }
}

复杂可辨识联合

// 事件系统
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string; code: string }
  | { type: "scroll"; deltaX: number; deltaY: number }
  | { type: "resize"; width: number; height: number };

function handleEvent(event: Event): void {
  switch (event.type) {
    case "click":
      console.log(`点击位置: (${event.x}, ${event.y})`);
      break;
    case "keypress":
      console.log(`按键: ${event.key} (${event.code})`);
      break;
    case "scroll":
      console.log(`滚动: (${event.deltaX}, ${event.deltaY})`);
      break;
    case "resize":
      console.log(`尺寸: ${event.width}x${event.height}`);
      break;
  }
}

never 类型与穷尽检查

type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
  switch (shape) {
    case "circle":
      return Math.PI * 100;
    case "square":
      return 100;
    case "triangle":
      return 50;
    default:
      // 穷尽检查:如果遗漏了某个 case,这里会报错
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// 如果后来添加了新类型 "pentagon",default 分支会报错:
// Type '"pentagon"' is not assignable to type 'never'

类型缩窄与守卫

// typeof 类型守卫
function process(value: string | number | boolean): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // value: string
  }
  if (typeof value === "number") {
    return value.toFixed(2);    // value: number
  }
  return value.toString();      // value: boolean
}

// instanceof 类型守卫
function formatDate(input: string | Date): string {
  if (input instanceof Date) {
    return input.toISOString(); // input: Date
  }
  return new Date(input).toISOString(); // input: string
}

// in 类型守卫
interface Bird { fly(): void; layEggs(): void; }
interface Fish { swim(): void; layEggs(): void; }

function move(animal: Bird | Fish): void {
  if ("fly" in animal) {
    animal.fly(); // animal: Bird
  } else {
    animal.swim(); // animal: Fish
  }
}

自定义类型守卫(Type Predicates)

// 返回类型是 pet is Fish(类型谓词)
function isFish(pet: Bird | Fish): pet is Fish {
  return "swim" in pet;
}

// 使用
const pet: Bird | Fish = getPet();

if (isFish(pet)) {
  pet.swim();  // ✅ pet: Fish
} else {
  pet.fly();   // ✅ pet: Bird
}

// 更多示例
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

// 数组过滤
const values: (string | null)[] = ["hello", null, "world", null];
const strings = values.filter(isNotNull); // string[]

断言函数(Assertion Functions)

// 断言函数:如果不满足条件则抛出错误
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function process(value: unknown): void {
  assertIsString(value);
  // 这里 value 已经被断言为 string
  console.log(value.toUpperCase()); // ✅
}

// assertNever 用于穷尽检查
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

type Status = "active" | "inactive";

function handleStatus(status: Status): string {
  switch (status) {
    case "active": return "活跃";
    case "inactive": return "未激活";
    default: return assertNever(status);
  }
}

业务场景:表单状态管理

// 使用可辨识联合描述表单状态
type FormState<T> =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "success"; data: T }
  | { status: "error"; errors: Record<string, string[]> };

interface User {
  id: number;
  name: string;
  email: string;
}

function renderFormState(state: FormState<User>): string {
  switch (state.status) {
    case "idle":
      return "请填写表单";
    case "submitting":
      return "提交中...";
    case "success":
      return `欢迎,${state.data.name}!`;
    case "error":
      const messages = Object.values(state.errors).flat();
      return `错误:${messages.join(", ")}`;
  }
}

// 使用
const state: FormState<User> = {
  status: "success",
  data: { id: 1, name: "Alice", email: "[email protected]" }
};

console.log(renderFormState(state));

注意事项

  1. 联合类型只能访问公共属性——需要类型收窄后才能访问特定类型的属性
  2. 可辨识联合是处理复杂联合类型的最佳实践
  3. as const 可以将值变为字面量类型,常与联合类型配合使用
  4. never 类型用于穷尽检查,确保处理了所有可能的情况
  5. 自定义类型守卫返回 value is Type,用于在条件分支中收窄类型

扩展阅读