这节在地图里是高级类型的"应用层"。TS 自带一批工具类型(Pick/Omit/Record/Exclude...),它们不是魔法,都是用前一节的映射与条件写出来的。读懂实现,你就不会误用;学会再造,你能写出贴合业务的专属工具。
interface User { id: number; name: string; email: string; role: string; } // 只取部分键 type UserPreview = Pick<User, "id" | "name">; // { id:number; name:string } // 排除部分键 type UserPublic = Omit<User, "email" | "role">; // { id:number; name:string }
Pick 实现就是映射 + 键约束:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
// 把一组键映射到同一值类型 type RolePermissions = Record<"admin" | "guest", string[]>; // 等价 { admin: string[]; guest: string[] } const perms: RolePermissions = { admin: ["read"], guest: [] };
Record<K, V> 适合"枚举键 + 统一值"的场景,如配置表、权限表。keyof any 约束保证 K 只能是 string|number|symbol。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" type T1 = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c" type T2 = NonNullable<string | null | undefined>; // string
Exclude 实现利用了分布式条件(5.1):
type Exclude<T, U> = T extends U ? never : T;
never 在联合里被吸收掉,于是"属于 U 的"被剔除。这套集合运算在筛选联合成员时极好用。
function greet(name: string, age: number): string { return ""; } type P = Parameters<typeof greet>; // [string, number] type R = ReturnType<typeof greet>; // string class Point { constructor(x: number, y: number) {} } type CP = ConstructorParameters<typeof Point>; // [number, number]
这些底层都是 infer(5.1)的功劳。实战里最常用 Parameters 把"某函数的入参"当作另一处的契约,避免重复声明。
这张图把常用工具类型的"血缘"归到两类实现:

// 需求:把对象的某些字段变成可空(用于"部分更新"DTO) type NullableKeys<T, K extends keyof T> = Omit<T, K> & { [P in K]: T[P] | null; }; interface Article { id: number; title: string; body: string; } type PatchArticle = NullableKeys<Article, "title" | "body">; // { id:number; title:string|null; body:string|null }
背景:PATCH 接口允许只传部分字段,但传了就要覆盖。操作:用 Omit 去掉目标键,再用映射加 | null 交回来。结果:一个精确描述"部分更新"的契约。解读:组合内置工具 + 映射,比从零写更稳。
TS 4.1 起,字符串字面量支持模板拼接,常用于事件名、路由等。
type Event = "click" | "hover"; type Handler = `on${Capitalize<Event>}`; // "onClick" | "onHover"
Capitalize/Uppercase/Lowercase/Uncapitalize 是内置的字符串转换。事件总线、CSS 属性名生成很适合它。
Awaited 与手写对照Promise 嵌套在 TS 4.5+ 有内置 Awaited 帮你剥到最里层,无需自己写递归。
async function load(): Promise<Promise<number>> { return Promise.resolve(1); } type R = Awaited<ReturnType<typeof load>>; // number,剥掉两层 Promise // 手写等价实现(递归条件类型) type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
背景:异步链常返回 Promise<Promise<...>>,直接用 Promise<X> 接会类型不符。操作:用 Awaited 或自写递归剥离。结果:拿到真实解析值类型。解读:内置 Awaited 比手写健壮(还处理 thenable),优先用它;理解手写版本则帮你掌握递归条件的套路。
DeepReadonly 的实战用法把"深层只读"做成工具,用在配置、快照这类"一旦创建不该变"的数据上。
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; interface AppConfig { db: { host: string; pool: number }; flags: { beta: boolean }; } const cfg: DeepReadonly<AppConfig> = { db: { host: "localhost", pool: 10 }, flags: { beta: true }, }; // cfg.db.host = "x"; // 报错:任意层级都只读
背景:全局配置注入后不希望被业务误改。操作:用 DeepReadonly 包一层。结果:从根到叶全锁住,比单层 Readonly 彻底。解读:这是映射类型 + 递归条件(5.1/5.2)组合的典型产物,体现"内置给积木,你拼出业务专属工具"的范式。
内置工具覆盖了 80% 需求,可读性好、团队都懂。自定义工具类型只在"反复出现且内置凑不出"时才写,且必须加注释说明意图和用例。乱造工具类型会让代码库出现一堆只有作者懂的抽象。
ReturnType 不只能看函数返回,还能把"一个函数的输出"直接当作"下一个函数的输入",避免重复声明中间类型。
function loadUser() { return { id: 1, name: "张三" } as const; } function format(u: ReturnType<typeof loadUser>): string { return `${u.id}:${u.name}`; } // 若 loadUser 返回加了字段,format 自动跟着变,无需手动同步
输入输出:format 的参数类型绑定到 loadUser 的返回,源头改动后下游自动更新。这比手写 interface User {...} 再两边维护更不容易脱节——前提是两处本就同源。
内置单个工具不够时,组合比造新工具稳。比如"把某对象里部分键变成可空":
// Omit 去掉目标键,再 Pick 出目标键范围,最后映射加 | null type PartialNullable<T, K extends keyof T> = Omit<T, K> & { [P in K]: T[P] | null }; interface Order { id: number; paid: boolean; note: string } type P = PartialNullable<Order, "paid" | "note">; // { id:number; paid:boolean|null; note:string|null }
背景:局部更新场景只让某些字段可空。操作:Omit + 映射组合。结果:精确描述"部分可空"的契约。解读:优先组合内置工具,是这节开头工程取舍的落地——可读性最高,团队一眼能拆懂。
⚠️ 别为了"显得会"而自定义工具类型。内置已覆盖大多数场景,自造抽象若没有清晰复用动机,只会增加维护负担。
💡 写自定义工具前,先翻一遍内置列表(Pick/Omit/Record/Exclude/Extract/Parameters...),多半能组合出来;组合不出的,再考虑从映射或条件手写。