TypeScript 映射类型:DRY 的类型编程
映射类型让 TypeScript 类型可以"批量加工":全变可选、全变只读、属性重命名。本文讲解映射类型语法、修饰符控制与可复用类型工具的构建。
映射类型用 { [K in keyof T]: T[K] } 语法遍历旧类型的每个属性生成新类型——内置的 Partial、Required、Readonly、Pick 全是它实现的。掌握它,你就掌握了 TypeScript 类型系统的"元编程"能力。
解决什么问题
interface User { id: number; name: string; email: string; }
// 需求:一个"更新用户"的类型,所有字段可选
// 笨办法:再写一遍,每个字段加 ? —— User 改了它就漂移
interface UserUpdate { id?: number; name?: string; email?: string; }
映射类型把"再写一遍"变成"一条规则":
type UserUpdate = { [K in keyof User]?: User[K] };
// 这就是内置 Partial<User> 的实现原理
User 加字段,UserUpdate 自动跟上——类型也 DRY。
修饰符的增与删
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 去掉只读
type Required2<T> = { [K in keyof T]-?: T[K] }; // 去掉可选(内置 Required)
type Nullable<T> = { [K in keyof T]: T[K] | null }; // 全加 null
+/- 前缀控制修饰符的添加与移除(+ 是默认)——readonly 和 ? 都可以双向操作。
键重映射:as 子句
// 属性名加前缀
type Prefixed<T, P extends string> = {
[K in keyof T as `${P & string}${Capitalize<string & K>}`]: T[K]
};
type ApiUser = Prefixed<User, 'api'>;
// { apiId: number; apiName: string; apiEmail: string }
// 过滤属性:键重映射为 never 即排除
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
键重映射(TS 4.1+)让映射类型从"批量改属性"升级为"筛选与重构属性集"。
构建可复用类型工具
// 深度只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
};
// 把指定键变可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 事件 Map 模式:为每个数据键生成变更回调
type ChangeHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void
};
这套"类型工具库"思路是大型 TS 项目基建的一部分——表单库、状态管理、API 层都在用。
与条件类型联用
映射类型遍历结构,条件类型做分支——两者合体即是类型编程的完整工具箱。内置的 Pick、Omit、Record 本质都是这个组合的封装。
常见问题(FAQ)
Q:keyof 拿到的是什么?
类型所有键的联合('id' | 'name' | 'email')。映射类型的 K in keyof T 就是对这个联合的遍历。
Q:映射类型会影响性能吗?
类型层面的计算只发生在编译期,运行时零成本。但超深的递归映射会拖慢编译——类型体操适可而止。
Q:什么时候不该用映射类型?
目标类型与源类型没有系统性关联时(比如 API 响应与前端模型差异巨大),硬用映射不如直接定义新接口清晰。映射类型服务的是"衍生关系明确"的场景。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。