Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/TypeScript 深度教程/第4章

第4章:类型建模 —— 联合、交叉、字面量与类型收窄

4 分钟 · 更新于 2026-09-01

第4章:类型建模 —— 联合、交叉、字面量与类型收窄

TypeScript 的核心能力不是给变量贴标签,而是把业务状态建模为可分析的类型。本章重点学习判别联合、类型守卫和穷尽检查,让非法状态更难进入程序。


一、从布尔字段陷阱开始

下面的模型允许大量矛盾状态:

ts
interface RequestState {
  loading: boolean;
  success: boolean;
  error?: string;
  data?: string[];
}

例如 loadingsuccess 同时为 true,或者成功却没有 data

改为判别联合:

ts
type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string[] }
  | { status: "error"; error: string };

每个状态只携带它真正需要的数据。

二、字面量类型

ts
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type RetryCount = 0 | 1 | 2 | 3;

字面量类型适合表达有限状态、协议值和配置选项。配合 as const 可从运行时数据生成类型:

ts
const cabins = ["economy", "business", "first"] as const;
type Cabin = (typeof cabins)[number];

此时运行时数组和编译时联合来自同一来源,减少重复。

三、类型别名

ts
type UserId = string;
type Coordinates = readonly [number, number];
type Callback<T> = (value: T) => void;

类型别名不会创造新的名义类型:

ts
type UserId = string;
type OrderId = string;
// 两者仍然兼容

需要区分时可以采用品牌类型:

ts
type Brand<T, Name extends string> = T & { readonly __brand: Name };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

品牌通常应通过受控构造函数产生,而不是到处断言。

四、联合类型

ts
type Identifier = string | number;

对联合值只能直接使用所有成员共有的能力。要使用成员专属方法,先收窄。

ts
function formatId(id: Identifier): string {
  return typeof id === "number" ? id.toFixed(0) : id.toUpperCase();
}

五、交叉类型

交叉类型表示同时满足多个类型:

ts
type Timestamped = { createdAt: Date; updatedAt: Date };
type User = { id: string; name: string };
type StoredUser = User & Timestamped;

交叉不是对象值的运行时合并。若同名属性无法兼容,结果可能变成 never

ts
type Impossible = { id: string } & { id: number };

六、内置类型守卫

typeof

适用于原始类型:

ts
if (typeof value === "string") {
  value.toUpperCase();
}

注意 typeof null === "object" 是 JavaScript 历史行为。

instanceof

适用于运行时类和构造函数:

ts
if (error instanceof Error) {
  console.error(error.message);
}

接口不能用于 instanceof,因为运行时不存在。

in

ts
type Cat = { meow(): void };
type Dog = { bark(): void };

function speak(animal: Cat | Dog): void {
  if ("meow" in animal) animal.meow();
  else animal.bark();
}

真值检查

真值检查方便,但可能误伤空字符串或数字 0。若它们是合法值,应明确比较 nullundefined

七、自定义类型谓词

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

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as Record<string, unknown>;
  return typeof candidate.id === "number" &&
    typeof candidate.name === "string";
}

类型谓词 value is User 是一项承诺。函数实现写错时,编译器不会替你证明它真实可靠,因此必须测试边界输入。

八、判别联合与穷尽检查

ts
type PaymentResult =
  | { kind: "success"; transactionId: string }
  | { kind: "rejected"; reason: string }
  | { kind: "pending"; pollUrl: string };

function describe(result: PaymentResult): string {
  switch (result.kind) {
    case "success":
      return `成功:${result.transactionId}`;
    case "rejected":
      return `拒绝:${result.reason}`;
    case "pending":
      return `处理中:${result.pollUrl}`;
    default:
      return assertNever(result);
  }
}

function assertNever(value: never): never {
  throw new Error(`未处理状态:${JSON.stringify(value)}`);
}

新增成员后,未更新的 switch 会产生类型错误。

九、类型断言的真实含义

ts
const element = document.querySelector("#app") as HTMLDivElement;

断言不会查询 DOM,也不会保证元素存在。更安全的写法:

ts
const element = document.querySelector("#app");
if (!(element instanceof HTMLDivElement)) {
  throw new Error("#app 不是 div");
}

双重断言 value as unknown as Target 基本是在绕过类型系统,应视为高风险信号。

十、satisfies:检查但保留精确类型

ts
const routes = {
  home: "/",
  user: "/users/:id"
} satisfies Record<string, string>;

与直接标注相比,satisfies 会检查对象符合目标约束,同时保留对象自身更精确的键和值信息。

十一、递归类型

ts
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

递归类型适合树、菜单、JSON 和嵌套评论。但深层类型运算会增加编译器负担,应优先可读性,不要把类型系统当通用编程语言无限递归。

十二、协变与逆变的实用理解

不必一开始背理论,先记住函数参数和返回值方向不同:

  • 返回值更具体通常可以赋给返回值更宽泛的位置。
  • 参数类型不能随意变窄,否则调用者可能传入实现无法处理的值。
ts
class Animal {}
class Dog extends Animal { bark() {} }

let createAnimal: () => Animal;
const createDog = (): Dog => new Dog();
createAnimal = createDog;

严格函数类型会帮助检查回调参数的不安全替换。

十三、常见误区

  1. 用多个布尔值表达互斥状态。
  2. 认为类型别名会创造新的运行时类型。
  3. 把交叉类型理解为 Object.assign
  4. 自定义类型守卫内部随意断言。
  5. as 消除所有报错,而不处理数据来源。
  6. 真值检查误把 0 或空字符串当缺失。
  7. 判别联合没有 never 穷尽检查。
  8. 设计过度复杂的递归类型,导致编译慢且错误难读。

十四、实践练习

  1. 把“加载/成功/失败”四字段模型改成判别联合。
  2. 为航班舱等建立 as const 数据源和派生联合类型。
  3. 编写 isUser,测试 null、数组、缺字段和错误字段类型。
  4. 为支付结果实现穷尽处理,新增 cancelled 后观察错误。
  5. 用品牌类型区分 UserIdOrderId,建立构造函数。

十五、总结

  1. 类型建模的目标是减少非法状态,而不是追求复杂语法。
  2. 字面量和联合类型适合有限状态与协议值。
  3. 判别联合让控制流、数据和穷尽检查协同工作。
  4. 类型守卫连接运行时检查与静态收窄。
  5. 类型断言只是信任声明,不是校验和转换。
  6. 交叉、递归和型变需要服务于真实模型,避免为高级而高级。

请继续阅读:第5章:集合、异步与错误边界。


原始资料引用


  • 第3章:函数与对象
  • 第5章:集合、异步与错误边界

本章目录
一、从布尔字段陷阱开始二、字面量类型三、类型别名四、联合类型五、交叉类型六、内置类型守卫七、自定义类型谓词八、判别联合与穷尽检查九、类型断言的真实含义十、satisfies:检查但保留精确类型十一、递归类型十二、协变与逆变的实用理解十三、常见误区十四、实践练习十五、总结原始资料引用Related Documents
苏ICP备2025204887号-2