本附录目标:把标准库里的每一个工具类型连同「等价手写实现」一起列出来,让你不再把它当成黑盒;再用七个真实项目里反复出现的类型模式,演示工具类型如何组合成可直接复用的类型积木。
附录 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 与 undefined | T & {} | 校验之后的参数类型 |
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 章看推导过程,最后才考虑自己从零写。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。