5.2 映射类型


5.2 映射类型

这节在地图里是类型编程的"循环结构"。映射类型让你遍历一个类型的所有键,逐键改写——生成只读版、可选版、必填版,全都一行搞定。它是内置工具类型(5.3)的实现基础,也是日常定制变体最高频的手段。

基本形态:遍历键

// 把每个属性变成只读 type Readonly<T> = { readonly [K in keyof T]: T[K]; }; interface User { id: number; name: string; } type ROUser = Readonly<User>; // { readonly id: number; readonly name: string; }

keyof T 取出所有键的联合,[K in ...] 遍历每个键,T[K] 取对应值类型。输入输出:User 的每个字段都被加上 readonly

加修饰符:可选与必填

// 全部变可选 type Partial<T> = { [K in keyof T]?: T[K]; }; // 全部变必填(去掉可选) type Required<T> = { [K in keyof T]-?: T[K]; };

-? 是"去掉可选性"。同理 -readonly 去掉只读。这是映射类型操作修饰符的语法。

改写值类型:键重映射

TS 4.1+ 支持键重映射,给键也做变换,比如加前缀。

type Prefixed<T, P extends string> = { [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]; }; interface Point { x: number; y: number; } type PPoint = Prefixed<Point, "coord">; // { coordX: number; coordY: number; }

as 子句用模板字面量类型(5.3 涉及)重命名键。Capitalize 把首字母大写,是内置的字符串字面量工具。

条件 + 映射:按类型筛键

组合条件类型,可只保留满足某条件的键。

// 只保留值为函数的键 type Methods<T> = { [K in keyof T as T[K] extends (...a: any[]) => any ? K : never]: T[K]; }; interface Obj { a: number; foo(): void; bar(): void; } type M = Methods<Obj>; // { foo(): void; bar(): void; }

原理:不满足条件的键被映射成 never,而 never 键在对象类型里会被剔除。这张图把映射的执行过程拆开:

05-02-fig01-2

实战:生成表单初始值类型

// 域模型 → 表单草稿(所有字段可选,且值包一层"未填") type Draft<T> = { [K in keyof T]?: T[K] | null; }; interface Product { id: number; title: string; price: number; } type ProductDraft = Draft<Product>; // 每个字段都可选且可空

背景:编辑表单初始时字段可能空。操作:用映射把每个字段变 T[K] | null 且可选。结果:表单状态类型和域模型同步演进,改模型自动改草稿。解读:这是映射类型最实用的场景之一——"派生变体"永远跟着源类型走,不会脱节。

递归映射:处理嵌套对象

映射类型能递归作用于嵌套对象,做出"深度"版本的变体,而不只是平层。

type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; }; interface Config { server: { host: string; port: number }; debug: boolean; } type D = DeepPartial<Config>; // 所有层级字段都可缺省:server 可只给 host,port 可省略

背景:配置合并、补丁更新常只需"部分 + 深层"地覆盖。操作:在映射值类型里对 object 递归调用自身。结果:每一层都可缺省,不只是第一层。解读:extends object 作为递归收口条件,值类型不再是对象就停,避免无限展开(见 5.1 递归终止)。

同时改键与值:重映射 + 条件

键重映射和值改写可以一起用,比如"把函数型键去掉,其余键转只读"。

type NonMethodReadonly<T> = { readonly [K in keyof T as T[K] extends (...a: any[]) => any ? never : K]: T[K]; }; interface Mixed { id: number; save(): void; name: string; } type N = NonMethodReadonly<Mixed>; // { readonly id: number; readonly name: string; }

背景:序列化对象时不想带方法,只要数据字段。操作:键重映射里把函数键映射成 never 剔除,其余加 readonly。结果:得到纯数据、只读的投影类型。解读:重映射 as 与值位置的改写是正交的两件事,组合后能表达"既筛键又改值"的复杂派生。

映射类型与 keyof 的只读保护

映射过程里,若想"保留原修饰符",用 +? / 不写修饰符即可;若想强制统一,显式加。

// 保留原可选性不变(默认行为) type Keep<T> = { [K in keyof T]: T[K] }; // 原可选字段仍是可选 // 强制全部必填(忽略源可选) type ForceRequired<T> = { [K in keyof T]-?: T[K] }; interface Opt { id?: number; name?: string; } type F = ForceRequired<Opt>; // { id: number; name: string } 可选被抹掉

背景:某些 ORM/表单库要求所有字段必填(由运行时校验兜底)。操作:用 -? 抹掉可选。结果:类型强制完整。解读:映射类型给了你"修饰符的完全控制权",要不要保留源语义由你决定,这是手写变体做不到的精细度。

工程取舍:映射 vs 手写变体

字段少的类型,手写 ReadonlyUser 也行,但一旦源类型改字段,手写变体就漏。映射类型让变体"自动派生",源变它变。代价是阅读门槛,所以约定:超过三个字段的变体一律用映射生成,不手写。

实战:给所有方法加 try 包裹的类型

映射类型能批量改写"值类型"——比如把每个方法都包成返回 Result 的形状。

type Result<T> = { ok: true; value: T } | { ok: false; error: string }; type SafeApi<T> = { [K in keyof T]: T[K] extends (...a: any[]) => infer R ? (...a: Parameters<Extract<T[K], (...a: any[]) => any>>) => Result<R> : T[K]; }; interface Api { get(): number; name: string; } type S = SafeApi<Api>; // S = { get(): Result<number>; name: string }

输入输出:get 方法被包成返回 Result<number>name 字段原样保留。这种"批量函数签名改写"在给存量 API 套错误容器时很有用,且原类型结构不丢。

实战:深只读与深可选

单层 Readonly/Partial 对嵌套对象无效(见第九章陷阱),用递归映射补齐。

type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; }; type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; }; interface Config { db: { host: string; port: number }; debug: boolean } type D = DeepReadonly<Config>; // D 的 db.host 也变 readonly,无法再赋值

背景:配置对象要整体冻结、或表单草稿要整体可空。操作:递归映射遇对象再递归。结果:DeepReadonly<Config> 的每个层级都只读。解读:递归映射是映射类型的进阶形态,但要注意无限递归——对含循环引用的类型会展开爆栈,实际项目给"已知有限深度"的对象用。

本节要点回顾

  • 映射类型用 keyof T + [K in ...] 遍历键
  • ?/-?/readonly/-readonly 操作修饰符
  • 键重映射 as 可改名,配合模板字面量
  • 条件 + 映射可按类型筛键(never 键被剔除)

⚠️ 别把映射类型的 T[K] 当成运行时取值。它是在类型层"引用成员类型",编译后整个映射类型消失,没有任何循环或赋值发生。

💡 凡是"某类型的变体"(只读、可选、表单草稿),都用映射类型从源派生,别手写;源类型加字段时,变体自动跟着变,少一整类脱节 bug。


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