《TypeScript编程入门》附录 B 内置工具类型与常用类型模式

本附录分两部分:先给出一份内置工具类型全解,逐个列出签名、作用、等价手写实现与典型用例;再整理品牌类型、Result、穷尽检查、Builder 链、类型安全 get/set 路径、事件映射表、DeepPartial 等常用类型模式,每个模式都配一段可直接运行的代码与适用场景。

本附录目标:把标准库里的每一个工具类型连同「等价手写实现」一起列出来,让你不再把它当成黑盒;再用七个真实项目里反复出现的类型模式,演示工具类型如何组合成可直接复用的类型积木。

附录 B 内置工具类型与常用类型模式

工具类型不是新语法。它们全部由第 9 章讲过的映射类型、条件类型与 infer 组合而成,标准库只是替你把最常用的组合写好了。本附录分两部分:第一部分是查阅表,第二部分是可复制的模式库。

内置工具类型全解

先看签名总览,语义与标准库 lib.es5.d.ts 等声明文件一致:

// 对象修饰
type Partial<T>  = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// 键挑选
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = { [P in K]: T };

// 集合运算
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {};

// 函数与构造器
type Parameters<T extends (...a: any) => any> =
  T extends (...a: infer P) => any ? P : never;
type ReturnType<T extends (...a: any) => any> =
  T extends (...a: any) => infer R ? R : any;
type ConstructorParameters<T extends abstract new (...a: any) => any> =
  T extends abstract new (...a: infer P) => any ? P : never;
type InstanceType<T extends abstract new (...a: any) => any> =
  T extends abstract new (...a: any) => infer R ? R : any;

// this 与异步(ThisParameterType / OmitThisParameter 见下表)
type Awaited<T> =
  T extends null | undefined ? T
    : T extends object & { then(onfulfilled: infer F, ...a: any): any }
      ? F extends (v: infer V, ...a: any) => any ? Awaited<V> : never
      : T;

// 字符串操作:由编译器内置,无法手写
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

逐项速查表:

工具类型作用等价手写实现典型用例
Partial<T>所有属性变可选{ [K in keyof T]?: T[K] }表单草稿、PATCH 请求体
Required<T>所有属性变必填{ [K in keyof T]-?: T[K] }校验完成后的配置对象
Readonly<T>所有属性变只读{ readonly [K in keyof T]: T[K] }常量表、冻结的配置
Pick<T, K>只保留指定键{ [P in K]: T[P] }列表项摘要、投影查询
Omit<T, K>去掉指定键Pick<T, Exclude<keyof T, K>>创建入参(去 id 与时间戳)
Record<K, T>构造键值映射{ [P in K]: T }字典、枚举到文案的映射
Exclude<T, U>从联合中剔除成员T extends U ? never : T从状态枚举去掉「已完成」
Extract<T, U>从联合中保留成员T extends U ? T : never取出事件名的子集
NonNullable<T>去掉 null 与 undefinedT & {}校验之后的参数类型
Parameters<F>取函数参数元组F extends (...a: infer P) => any ? P : never包装函数、日志代理
ReturnType<F>取函数返回类型F extends (...a: any) => infer R ? R : any从实现反推契约类型
ConstructorParameters<C>取构造器参数元组条件类型 + infer P依赖注入容器、工厂
InstanceType<C>取实例类型条件类型 + infer R从类引用推导实例
Awaited<T>递归解开 Promise递归条件类型(见上)异步函数返回值类型
ThisParameterType<F>取 this 参数类型条件类型 + infer U给旧式回调补类型
OmitThisParameter<F>去掉 this 参数条件类型剥离首个参数把方法当自由函数传递
ThisType<T>指定对象字面量中的 this编译器特殊处理,无运行时实现Object.defineProperties、选项式 API
Uppercase<S>全大写编译器内置,不可手写生成 GET / POST 常量
Lowercase<S>全小写编译器内置,不可手写规范化 HTTP 方法
Capitalize<S>首字母大写编译器内置,不可手写生成 setName 这类方法名
Uncapitalize<S>首字母小写编译器内置,不可手写由事件名生成属性名

几条必须记住的性质:

  • Partial / Required / Readonly 都是浅层的。嵌套对象的内层不会跟着变,深层需求要写递归类型(见后文 DeepPartial)。
  • Pick 的键受 keyof T 约束,Omit 的键不受约束。拼错键名时 Omit 静默通过,这是最常见的隐性 bug。
  • Exclude / Extract / NonNullable 依赖条件类型的分发行为,因此对联合类型最有用,对普通对象类型几乎是恒等变换。
  • Awaited 会递归剥掉多层 Promise,手写的单层 infer 版本在 Promise<Promise<T>> 上会出错。
  • 四个字符串工具类型由编译器内置,无法用条件类型模拟。
  • Parameters / ReturnType 等函数类工具类型要求实参是函数,传类本身会报错,需改用 ConstructorParameters / InstanceType。

组合用法示例:

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

type CreateUserInput = Omit<User, "id" | "createdAt">;  // 创建入参
type UpdateUserInput = Partial<CreateUserInput>;        // 更新入参
type UserSummary = Pick<User, "id" | "name">;           // 列表项
type UsersByStatus = Record<"active" | "banned", UserSummary[]>;

declare function fetchUser(id: string): Promise<User>;
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;  // User

常用类型模式

下面七个模式覆盖了实际项目里绝大多数「需要写类型体操」的场景。每个模式都给出可直接粘贴运行的代码。

品牌类型(Branded Type)

结构化类型下 string 就是 string,用户 ID 和订单 ID 可以互相赋值。品牌类型用交叉一个假字段来区分:

declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

declare const uid: UserId;
declare const oid: OrderId;

const ok: UserId = uid;
const bad: UserId = oid;
// 错误:Type 'OrderId' is not assignable to type 'UserId'.

function asUserId(raw: string): UserId {
  return raw as UserId;   // 只在边界处断言一次
}

适用场景:ID、金额、经过校验的邮箱等「语义上不可互换」的原始值。

Result<T, E>

用判别联合替代抛异常,把错误路径纳入类型系统:

type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

function parsePort(input: string): Result<number, string> {
  const n = Number(input);
  if (!Number.isInteger(n) || n < 0 || n > 65535) {
    return { ok: false, error: `非法端口:${input}` };
  }
  return { ok: true, value: n };
}

const r = parsePort("8080");
if (r.ok) {
  console.log(r.value + 1);    // number
} else {
  console.log(r.error.length);  // string
}

适用场景:可预期的业务失败(校验、解析、外部调用),避免用异常做流程控制。

穷尽检查(Exhaustive Check)

判别联合加 never 兜底,让新增分支时编译器强制你处理:

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; size: number }
  | { kind: "rect"; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.size ** 2;
    case "rect":   return s.w * s.h;
    default: {
      const _exhaustive: never = s;
      throw new Error(`未处理的分支: ${JSON.stringify(_exhaustive)}`);
    }
  }
}

适用场景:状态机、action 分发、协议消息处理。

Builder 链式类型

让链式调用在类型层面记住「已经设置了什么」,漏设置就编译不过:

type Config = { host?: string; port?: number; tls?: boolean };

class ServerBuilder<Done extends boolean = false> {
  private cfg: Config = {};

  host(v: string): ServerBuilder<Done> {
    return Object.assign(this, { cfg: { ...this.cfg, host: v } });
  }
  port(v: number): ServerBuilder<Done> {
    return Object.assign(this, { cfg: { ...this.cfg, port: v } });
  }
  build(this: ServerBuilder<true>): Config {
    return this.cfg;
  }
}

declare const b: ServerBuilder;
// b.build();
// 错误:The 'this' context of type 'ServerBuilder<false>' is not assignable
// to method's 'this' of type 'ServerBuilder<true>'.

适用场景:必填项较多的配置对象、测试数据构造器、查询语句构建器。

类型安全的 get / set 路径

用模板字面量类型把 "user.address.city" 这样的点路径映射成正确的取值类型:

type Paths<T> = T extends object
  ? { [K in keyof T & string]: K | `${K}.${Paths<T[K]>}` }[keyof T & string]
  : never;

type PathValue<T, P extends string> =
  P extends `${infer K}.${infer Rest}`
    ? K extends keyof T ? PathValue<T[K], Rest> : never
    : P extends keyof T ? T[P] : never;

function get<T, P extends Paths<T>>(obj: T, path: P): PathValue<T, P> {
  return path.split(".").reduce<unknown>(
    (acc, k) => (acc as Record<string, unknown>)[k],
    obj,
  ) as PathValue<T, P>;
}

interface State {
  user: { name: string; address: { city: string } };
}

declare const s: State;
const city = get(s, "user.address.city");   // string
// get(s, "user.address.zip");
// 错误:Argument of type '"user.address.zip"' is not assignable ...

适用场景:表单库、状态库的路径读写、i18n 键名校验。

事件映射表

把「事件名 → 载荷类型」写成一张表,on / emit 两端都自动获得类型:

interface EventMap {
  login: { userId: string };
  logout: { reason: "manual" | "expired" };
  message: { from: string; text: string };
}

type Handler<M, K extends keyof M> = (payload: M[K]) => void;

class Emitter<M extends object> {
  private handlers: { [K in keyof M]?: Handler<M, K>[] } = {};

  on<K extends keyof M>(type: K, fn: Handler<M, K>): void {
    (this.handlers[type] ??= []).push(fn);
  }
  emit<K extends keyof M>(type: K, payload: M[K]): void {
    this.handlers[type]?.forEach((fn) => fn(payload));
  }
}

const bus = new Emitter<EventMap>();
bus.on("login", (p) => console.log(p.userId));
bus.emit("login", { userId: "u1" });
// bus.emit("login", { userId: 1 });
// 错误:Type 'number' is not assignable to type 'string'.

适用场景:前端事件总线、WebSocket 消息分发、插件钩子。

DeepPartial 与 DeepReadonly

两者同构,只是把修饰符换成 ? 与 readonly:

type DeepPartial<T> = T extends (...a: never[]) => unknown
  ? T
  : T extends readonly unknown[]
    ? { [K in keyof T]: DeepPartial<T[K]> }
    : T extends object
      ? { [K in keyof T]?: DeepPartial<T[K]> }
      : T;

type DeepReadonly<T> = T extends (...a: never[]) => unknown
  ? T
  : T extends readonly unknown[]
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

interface AppConfig {
  server: { host: string; port: number };
  db: { url: string; pool: { min: number; max: number } };
}

function merge(base: AppConfig, patch: DeepPartial<AppConfig>): AppConfig {
  return { ...base, ...patch } as AppConfig;
}

declare const store: DeepReadonly<AppConfig>;
// store.server.host = "x";
// 错误:Cannot assign to 'host' because it is a read-only property.

适用场景:配置覆盖、Redux 式只读状态、跨模块共享的常量对象。

小结

  • 工具类型全部由映射类型、条件类型与 infer 组合而成,本附录给出的手写实现与标准库语义一致,可以放心对照阅读。
  • Partial / Required / Readonly 是浅层修饰,深层需求必须写递归版本;Omit 的键不受约束,是最容易埋雷的一个。
  • 四个字符串工具类型由编译器内置,无法手写也无法扩展,需配合模板字面量类型才发挥价值。
  • 类型模式的共同目标只有一个:把「非法状态」变成无法表达的类型——品牌类型排除语义混用,判别联合排除状态组合爆炸,穷尽检查排除漏分支。
  • 品牌类型与 DeepReadonly 都只在类型层生效,运行时没有任何开销,可放心用在热路径上。
  • 遇到更复杂的类型需求,先在本附录找最接近的模式,再回第 9 章与第 10 章看推导过程,最后才考虑自己从零写。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「typescript」更多文章

  1. 《TypeScript高级编程》11.3 类型驱动架构与团队规范
  2. 《TypeScript高级编程》11.2 渐进式迁移与严格化路径
  3. 《TypeScript高级编程》11.1 TS 版本演进与 breaking changes