2Programming TypeScript

类型系统

类型深入

联合类型交叉类型字面量类型枚举

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章:类型系统(The Type System)

一、导读

1.1 本章学习目标

第2章深入 TypeScript 的类型系统——这是 TypeScript 最核心的部分。你将学习联合类型、交叉类型、字面量类型、枚举等高级类型概念,理解 TypeScript 如何通过类型组合来表达复杂的业务逻辑。

本章的核心学习目标包括:

  • 掌握联合类型(Union Types):一个值可以是多种类型之一
  • 理解交叉类型(Intersection Types):合并多个类型为一个
  • 掌握字面量类型(Literal Types):精确到具体值的类型
  • 理解枚举类型(Enum Types):命名常量的集合
  • 学会使用类型别名(Type Aliases)创建可复用的类型
  • 掌握类型收窄(Type Narrowing)的各种方法

1.2 为什么本章如此重要

TypeScript 的类型系统不仅仅是"给变量加个类型标注"那么简单。它的类型系统是一个强大的领域建模工具——通过组合基本类型,你可以精确地描述业务规则,让编译器帮你验证数据的合法性。

比如,一个 HTTP 响应要么是成功(带数据),要么是失败(带错误码)——用联合类型可以精确表达这种"互斥"关系。一个用户配置对象需要同时满足多个接口的约束——用交叉类型可以组合这些约束。一个状态机只有有限的几个状态——用字面量联合类型可以穷举所有可能。

1.3 核心问题引导

联合类型 A | B 和交叉类型 A & B 有什么区别?各自适用于什么场景?

字面量类型 "GET" | "POST" 和 string 有什么区别?为什么前者更安全?

TypeScript 的枚举有什么争议?为什么有些人推荐使用联合类型替代枚举?

类型收窄(narrowing)是如何工作的?typeof、in、自定义类型守卫有什么区别?

如何设计"不可表示非法状态"的类型?


二、核心概念详解

2.1 联合类型(Union Types)

2.1.1 基本概念

联合类型表示一个值可以是多种类型之一:

typescript
type StringOrNumber = string | number;

function formatValue(value: StringOrNumber): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

formatValue("hello");    // "HELLO"
formatValue(3.14159);    // "3.14"

2.1.2 联合类型的收窄

访问联合类型的属性或方法时,只能访问所有成员类型共有的部分:

typescript
type Pet = { name: string; type: "dog" } | { name: string; type: "cat"; declawed: boolean };

function processPet(pet: Pet) {
  console.log(pet.name);    // OK,所有成员都有 name
  // console.log(pet.declawed);    // 错误!不是所有成员都有 declawed
  
  if (pet.type === "cat") {
    console.log(pet.declawed);    // OK!TypeScript 收窄了类型
  }
}

2.2 交叉类型(Intersection Types)

交叉类型将多个类型合并为一个,新类型拥有所有成员类型的属性:

typescript
interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

type Person = HasName & HasAge;

const alice: Person = {
  name: "Alice",
  age: 30
};

// 实际应用场景:组合 mixins
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };

type User = { id: string; name: string } & Timestamped & SoftDeletable;

2.3 字面量类型(Literal Types)

字面量类型将类型的范围缩小到具体的值:

typescript
// 字符串字面量类型
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction, distance: number) {
  // ...
}

move("north", 5);    // OK
// move("up", 5);    // 错误!"up" 不是 Direction

// 数字字面量类型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

// 布尔字面量类型
type True = true;
type False = false;

2.3.1 判别联合类型(Discriminated Unions)

字面量类型最强大的应用是判别联合类型——使用一个字面量属性作为"判别器":

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; sideLength: number }
  | { kind: "triangle"; base: number; height: number };

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

2.4 枚举类型

2.4.1 数字枚举

typescript
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right    // 3
}

let dir: Direction = Direction.Up;

2.4.2 字符串枚举

typescript
enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE"
}

function request(method: HttpMethod, url: string) {
  // ...
}

request(HttpMethod.GET, "/api/users");

2.4.3 枚举的替代方案

很多 TypeScript 开发者推荐使用联合类型替代枚举:

typescript
// 替代方案1:联合类型
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

// 替代方案2:const 对象
const HttpMethod = {
  GET: "GET",
  POST: "POST",
  PUT: "PUT",
  DELETE: "DELETE"
} as const;

type HttpMethodType = typeof HttpMethod[keyof typeof HttpMethod];

2.5 类型收窄(Type Narrowing)

2.5.1 typeof 收窄

typescript
function process(value: string | number | boolean) {
  if (typeof value === "string") {
    value.toUpperCase();    // value 是 string
  } else if (typeof value === "number") {
    value.toFixed(2);       // value 是 number
  } else {
    !value;                 // value 是 boolean
  }
}

2.5.2 in 收窄

typescript
interface Fish { swim(): void }
interface Bird { fly(): void }

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

2.5.3 自定义类型守卫

typescript
interface Cat { meow(): void; purr(): void }
interface Dog { bark(): void; fetch(): void }

function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function makeSound(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow();    // animal 是 Cat
  } else {
    animal.bark();    // animal 是 Dog
  }
}

2.6 类型别名

typescript
// 基本类型别名
type ID = string | number;

// 复杂类型别名
type Callback<T> = (error: Error | null, result: T) => void;

// 递归类型别名
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };

// 配合泛型
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

三、实战案例

3.1 案例一:类型安全的状态机

typescript
type ConnectionState =
  | { status: "disconnected" }
  | { status: "connecting"; attempt: number }
  | { status: "connected"; sessionId: string }
  | { status: "error"; message: string; retryable: boolean };

class ConnectionManager {
  private state: ConnectionState = { status: "disconnected" };

  transition(next: ConnectionState) {
    const valid = this.isValidTransition(this.state, next);
    if (!valid) {
      throw new Error(`Invalid transition: ${this.state.status} -> ${next.status}`);
    }
    this.state = next;
  }

  private isValidTransition(from: ConnectionState, to: ConnectionState): boolean {
    switch (from.status) {
      case "disconnected":
        return to.status === "connecting";
      case "connecting":
        return to.status === "connected" || to.status === "error";
      case "connected":
        return to.status === "disconnected" || to.status === "error";
      case "error":
        return to.retryable ? to.status === "connecting" : to.status === "disconnected";
    }
  }

  getStatus(): string {
    switch (this.state.status) {
      case "disconnected":
        return "Disconnected";
      case "connecting":
        return `Connecting (attempt ${this.state.attempt})`;
      case "connected":
        return `Connected (session: ${this.state.sessionId})`;
      case "error":
        return `Error: ${this.state.message}`;
    }
  }
}

3.2 案例二:类型安全的表单验证

typescript
type ValidationRule<T> = {
  field: keyof T;
  validate: (value: T[keyof T]) => string | null;
};

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

const rules: ValidationRule<LoginForm>[] = [
  {
    field: "email",
    validate: (value) => {
      if (typeof value !== "string" || !value.includes("@")) {
        return "Invalid email address";
      }
      return null;
    }
  },
  {
    field: "password",
    validate: (value) => {
      if (typeof value !== "string" || value.length < 8) {
        return "Password must be at least 8 characters";
      }
      return null;
    }
  }
];

function validate<T extends object>(form: T, rules: ValidationRule<T>[]): Record<keyof T, string | null> {
  const errors = {} as Record<keyof T, string | null>;
  for (const key of Object.keys(form) as (keyof T)[]) {
    errors[key] = null;
  }
  
  for (const rule of rules) {
    const error = rule.validate(form[rule.field]);
    if (error) {
      errors[rule.field] = error;
    }
  }
  
  return errors;
}

3.3 案例三:类型安全的 API 响应处理

typescript
type ApiResponse<T> =
  | { status: 200; data: T }
  | { status: 400; error: string }
  | { status: 401; error: string; redirectUrl: string }
  | { status: 404; error: string }
  | { status: 500; error: string; traceId: string };

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

async function fetchUser(id: string): Promise<User | null> {
  const response: ApiResponse<User> = await apiCall(`/users/${id}`);
  
  switch (response.status) {
    case 200:
      return response.data;
    case 401:
      window.location.href = response.redirectUrl;
      return null;
    case 404:
      console.warn(`User ${id} not found`);
      return null;
    case 400:
      throw new Error(`Bad request: ${response.error}`);
    case 500:
      throw new Error(`Server error: ${response.error} (trace: ${response.traceId})`);
  }
}

四、本章小结

第2章深入讲解了 TypeScript 的类型系统:

联合类型:A | B 表示值可以是 A 或 B,通过类型收窄访问特定成员。

交叉类型:A & B 合并多个类型的属性。

字面量类型:将类型范围缩小到具体值,是判别联合类型的基础。

枚举:命名常量集合,但联合类型通常是更好的替代方案。

类型收窄:typeof、in、自定义类型守卫让编译器理解类型的具体形态。


五、思考与练习

思考题

联合类型和交叉类型在集合论中分别对应什么操作?

为什么判别联合类型(Discriminated Unions)如此重要?它如何帮助实现"不可表示非法状态"?

TypeScript 的枚举有什么缺陷?为什么社区倾向于使用联合类型替代?

如何设计一个类型系统,使得"不可能的状态不可表示"?

编程练习

使用判别联合类型实现一个购物车状态:空购物车、有商品的购物车、已结账的购物车。

编写一个类型安全的 EventEmitter,确保事件名和回调参数类型匹配。

实现一个类型安全的 Result<T, E> 类型,包含 Ok 和 Err 两个变体。


2/7类型系统
0%