先记住这个答案
映射类型用类似 { [K in Keys]: ValueType } 的形式,为键集合中的每个键计算属性类型。Keys 可以是有限键联合,也常来自 keyof T;需要保留每个字段与原值的关系时,可以在结果位置使用 T[K]。普通字符串索引签名主要描述任意字符串键的值约束,而有限映射可以要求指定键都存在。同态形式还会保留原字段的只读与可选信息,除非显式调整修饰符。这些都是静态类型运算,不会自动遍历对象、创建字段或执行深层数据转换。
- 映射从键集合逐项计算属性类型
- 有限键映射可以要求指定字段齐全
- 修饰符与运行时对象构造都需要明确处理
为什么有限映射比宽索引更具体
功能开关如果只允许搜索和导出两项,有限键联合可以让配置明确包含这两项。宽字符串索引允许任意键,却不一定要求这些指定键都出现。选择取决于协议是开放字典还是固定字段集合,不能只根据写法相似就互换。
示例先创建固定开关结构,再把用户字段映射为带 value 的包装对象。第二种形式保留了每个键对应的原字段类型,也延续只读和可选修饰。漏掉必需开关或写入只读字段会得到预期错误,说明映射结果仍是一份完整可检查的契约。
type Feature = 'search' | 'export';
type Flags = { [K in Feature]: boolean };
type Wrapped<T> = { [K in keyof T]: { value: T[K] } };
type User = { readonly id: number; nickname?: string };
const flags: Flags = { search: true, export: false };
// @ts-expect-error export 是必需开关
const missing: Flags = { search: true };
const wrapped: Wrapped<User> = { id: { value: 7 } };
// @ts-expect-error 同态映射保留 id 的只读修饰
wrapped.id = { value: 8 };固定映射要求两个开关齐全,Wrapped 保留 id 的数字关系和只读属性,nickname 仍可省略。包装对象由实际字面量创建,类型映射本身不会把原 User 自动转换成这份值。
修饰符为何不是可忽略的小细节
映射可以显式增加或移除 readonly 和可选标记,常见工具会利用这些能力调整更新接口。但去掉可选标记只是改变静态要求,不会自动给缺失字段补默认值;把只读字段变可写也不会复制数据或解除运行时冻结。
如果映射仅处理对象的第一层,嵌套字段仍保持原来的结构与可变性。所谓深度只读或深度可选需要额外递归设计,还要决定函数、数组、日期和循环结构如何处理。不能因为用了映射语法就宣称得到完整深层转换。
把类型派生与值派生保持一致
运行时转换函数若承诺返回映射后的对象,仍必须实际遍历或构造全部必需字段,并处理原对象的键规则。Object.keys 不包含符号键,结构类型还可能允许额外字段,因此一个泛型循环是否真的对应完整映射,需要单独审查。
类型映射最适合减少明确结构间的重复声明,例如字段错误、包装值或固定权限表。应让键集合来自稳定事实来源,并在源模型变化时检查实现是否同步更新。编译器可以发现部分契约不匹配,却不会替转换函数生成缺失的业务逻辑。
容易答错的地方
- 映射类型会自动生成对应的对象字段
- 它只产生静态结构,运行时对象仍由代码创建;若声明要求新增字段,实际构造逻辑也必须提供,不能只修改类型别名。
- 映射结果天然都是必需可写字段
- 同态形式会保留原有修饰,其他写法也可显式添加或移除;应查看 readonly 与可选标记,不要只关注字段值类型。
面试官还会怎么问?
键联合可以包含数字和符号吗?
合法属性键类型可以包含字符串、数字和符号,具体映射应符合这些规则;如果后续要拼接字符串键名,需要另外限制或转换键类型。
如何把可选字段改成必需字段?
可以使用移除可选标记的映射修饰符,但这只提高结果类型的要求;运行时必须确认字段已经存在,或提供真实默认值与校验。
映射能直接重命名每个属性吗?
可以通过 as 子句计算新键,或者用 never 过滤部分键;这属于键重映射,需要额外考虑同名冲突和字符串键处理规则。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。