先记住这个答案
映射类型可以在遍历键后用 as 子句计算结果键,例如根据字段名生成 getName,也可以让某些键变成 never 以将它们移除。模板字面量通常需要明确处理字符串键,不能忽略 keyof 还可能含有数字或符号。多个源键映射到同名目标键时,结果不会像 JavaScript 对象赋值那样简单以后者覆盖前者,相关值类型可能合并。类型声明只描述新结构,实际创建 getter、删除字段或处理命名冲突仍需要运行时实现。
- as 计算结果属性名,never 可以过滤键
- 字符串模板需要明确数字和符号键的处理
- 重命名后的类型不会自动实现对应方法
如何从字段名生成方法名
表单或状态访问层可能希望把 name 字段变成 getName 方法。as 子句负责计算键,值位置再使用 T[K] 描述方法返回类型,这样字段从字符串改成数字时,派生方法的返回契约也能随之改变。
示例明确过滤 secret,并只为字符串键生成 getter。数字与符号键在这个接口中被排除,这是工具主动选择的规则,而不是它们不能作为对象键。实际对象仍手写两个方法,用来说明类型派生与方法实现各自承担的职责。
type PublicGetters<T> = {
[K in keyof T as K extends 'secret' ? never :
K extends string ? `get${Capitalize<K>}` : never]: () => T[K]
};
type User = { name: string; age: number; secret: string };
const getters: PublicGetters<User> = {
getName: () => 'Lin',
getAge: () => 28
};
// @ts-expect-error secret 已从目标键中排除
getters.getSecret();getName 返回字符串,getAge 返回数字,过滤掉的字段不会产生 getSecret 契约。代码中的函数实现是显式创建的,映射类型既不会生成方法体,也不会删除其他真实对象上的敏感字段。
同名冲突为什么不能按覆盖理解
大小写转换、移除前缀或统一命名都可能把两个不同源键变成同一目标键。类型层合并候选与运行时对象构造的覆盖顺序不是同一规则,结果可能要求或允许一组联合值,而具体实现只保留最后一次写入。两者不一致会产生隐藏错误。
设计通用转换器时,应先规定命名冲突是报错、拒绝输入还是采用确定合并策略,并用实际转换测试验证。若类型层无法清楚表达所有冲突规则,限制支持的输入形状通常比用宽断言假装全部自动安全更可靠。
过滤类型不等于过滤真实数据
将 secret 从派生类型里移除,只改变后续可见的静态接口,不会把原对象上的 secret 字段擦掉。序列化、日志或对象展开仍可能携带它。需要数据脱敏时,应执行实际白名单构造,并确认嵌套结构也符合输出协议。
键重映射与模板字面量从 TypeScript 4.1 起支持。公共库需要考虑消费端语法版本和复杂映射带来的编译成本。日常代码应让派生规则容易阅读,避免把命名、过滤、递归和条件分发全部塞在一个难以验证的表达式里。
容易答错的地方
- 把字段映射成 never 会删除对象中的真实字段
- 它只移除结果类型里的键,实际对象仍可能携带原值;涉及输出白名单或敏感数据时必须执行真实字段选择。
- 两个键映射重名会按类型声明顺序覆盖
- 类型重映射的候选合并不等于 JavaScript 赋值顺序,运行实现与结果类型可能因此不一致;应明确冲突政策并验证实际输入。
面试官还会怎么问?
为什么示例先限制 K 是 string?
keyof 可能包含数字和符号,而 Capitalize 等字符串运算需要字符串类型;工具应明确是排除其他键、保留它们还是执行特定转换。
as 与普通类型断言的 as 一样吗?
不是同一用途,映射里的 as 在计算结果键,表达式中的类型断言则让编译器按指定类型看待某个值;应根据语法位置理解。
可以按字段值类型筛选键吗?
可以在新键位置使用条件类型,根据 T[K] 决定保留 K 还是返回 never;同时需要考虑可选字段和联合值类型是否符合预期筛选规则。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。