5.3 内置工具类型与自定义


5.3 内置工具类型与自定义

这节在地图里是高级类型的"应用层"。TS 自带一批工具类型(Pick/Omit/Record/Exclude...),它们不是魔法,都是用前一节的映射与条件写出来的。读懂实现,你就不会误用;学会再造,你能写出贴合业务的专属工具。

子集选取:Pick 与 Omit

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]; };

键到值的表:Record

// 把一组键映射到同一值类型 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

集合运算:Exclude / Extract / NonNullable

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 的"被剔除。这套集合运算在筛选联合成员时极好用。

函数与构造:Parameters / ConstructorParameters / ReturnType

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 把"某函数的入参"当作另一处的契约,避免重复声明。

这张图把常用工具类型的"血缘"归到两类实现:

05-03-fig01-2

自定义业务工具:举个实例

// 需求:把对象的某些字段变成可空(用于"部分更新"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 串起调用链契约

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/Parameters 源于条件+infer
  • Record 适合枚举键配统一值
  • Parameters/ReturnType 复用函数签名,避免重复声明
  • 模板字面量类型让字符串也能参与类型运算

⚠️ 别为了"显得会"而自定义工具类型。内置已覆盖大多数场景,自造抽象若没有清晰复用动机,只会增加维护负担。

💡 写自定义工具前,先翻一遍内置列表(Pick/Omit/Record/Exclude/Extract/Parameters...),多半能组合出来;组合不出的,再考虑从映射或条件手写。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U