- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
映射类型(Mapped Types)是 TypeScript 类型系统中"以类型生成类型"的核心能力之一,它允许你在编译期遍历现有类型的每一个属性,并通过映射函数将其转换为全新的类型结构。本文以《The Concise TypeScript Book》开源仓库中 西班牙语版映射类型章节 为主线,结合仓库内 映射类型修饰符、索引签名、模板联合类型 与 类型操作总览 等章节,带你从零掌握映射类型的语法、修饰符、键重映射与内置工具类型的底层实现,并能在实际项目中用它写出可复用的类型工具。
什么是映射类型:从已有类型推导新类型
在 TypeScript 中,类型系统不是静态封闭的——你可以基于一个已有的类型,通过变换其每个属性来构造出全新的类型。这就是映射类型的核心思想:访问现有类型的属性集合,再逐一对属性进行变换,最终产生一个表达相同信息、但结构不同的新类型。
访问属性集合的关键运算符是keyof,它返回一个对象类型所有属性名的联合类型。例如:
type MyType = { foo: string; bar: number; }; // keyof MyType 等价于 "foo" | "bar"有了属性名的联合类型,就可以用[P in keyof T]的语法逐一遍历每个属性,形成映射。仓库中 映射类型章节 给出的权威示例如下:
type MyMappedType<T> = { [P in keyof T]: T[P][]; }; type MyType = { foo: string; bar: number; }; type MyNewType = MyMappedType<MyType>; const x: MyNewType = { foo: ['hello', 'world'], bar: [1, 2, 3], };我们来逐步拆解这段代码的执行过程:
keyof T得到MyType的属性名联合"foo" | "bar";P in keyof T表示遍历P,每次取一个属性名;T[P][]是索引访问类型(Indexed Access Type)与数组后缀的组合,表示"取属性P的类型T[P],再包一层数组";- 于是
MyNewType展开后等价于{ foo: string[]; bar: number[] }。
MyNewType与MyType承载的信息量一致(同样的两个字段),但每个字段的值形态从string/number变成了string[]/number[]。这正是映射类型的价值:同一份数据,用不同的类型形态去表达,且变换过程完全由编译器推导,无需手写重复的接口定义。
支撑映射类型的三个基石:keyof、索引访问与索引签名
keyof 运算符
keyof是映射类型的入口,它把对象的属性名提炼成字面量联合类型。没有keyof,就无法遍历属性。它是连接"对象类型"与"属性名字符串集合"的桥梁,也是后续所有映射、修饰符、键重映射操作的前提。
索引访问类型 Type[Key]
T[P]的写法即索引访问类型(Indexed Access Types),在 类型操作章节 中有专门讲解:
type Person = { name: string; age: number; }; type AgeType = Person['age']; // number它同样支持对元组按索引取值:
type MyTuple = [string, number, boolean]; type MyType = MyTuple[2]; // boolean映射类型中的T[P]正是这一特性的泛化应用——P不是固定的字面量,而是遍历中的变量。
索引签名:string / number / symbol
当被映射的类型带有索引签名时,映射规则会作用于索引签名字段上。索引签名章节 说明 TypeScript 支持string、number、symbol三类索引:
type K = { [name: string | number]: string; }; const k: K = { x: 'x', 1: 'b' }; console.log(k['x']); console.log(k[1]); console.log(k['1']); // Same result as k[1]注意一个 JavaScript 层面的行为:数字索引会被自动转换为字符串索引,因此k[1]与k["1"]返回的是同一个值。理解这一点,有助于理解为什么映射到带数字索引的类型时,键的联合类型中数字与字符串字面量可能表现出一致性。
映射类型修饰符:readonly、-readonly 与可选 ?
映射类型不仅能把属性值换成别的类型,还能变换属性本身的"修饰状态"。仓库的 映射类型修饰符章节 给出了三个修饰符:
| 修饰符 | 含义 |
|---|---|
readonly或+readonly | 将映射结果中的属性标记为只读 |
-readonly | 将映射结果中的属性还原为可写(可变) |
? | 将映射结果中的属性标记为可选 |
对应示例:
type ReadOnly<T> = { readonly [P in keyof T]: T[P] }; // 所有属性变为只读 type Mutable<T> = { -readonly [P in keyof T]: T[P] }; // 所有属性变为可写 type MyPartial<T> = { [P in keyof T]?: T[P] }; // 所有属性变为可选+前缀是可省略的默认写法,因此readonly [P in keyof T]与+readonly [P in keyof T]等价;而-readonly的作用恰好相反,用于"解除"只读。
关于只读的含义,只读属性章节 有一个重要提醒:readonly阻止的是"重新写入属性"这一操作,它并不提供整体不可变的保证——深层嵌套对象的内部字段依然可能被修改。使用时要有清晰的预期。
可选项的配合同样关键,可选属性章节 展示了带默认值的解构写法:
type X = { a: number; b?: number; }; const x = ({ a, b = 100 }: X) => a + b;修饰符的实战组合
修饰符可以自由组合,例如"把某个类型的所有属性变为可选且只读":
type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P]; };而"去除可选标记"则用-?语法(与-readonly对称):
type Required<T> = { [P in keyof T]-?: T[P] };-前缀的作用就是"移除修饰符",它同时适用于readonly与?,这也是 TypeScript 内置工具类型Required<T>和Mutable类工具的实现原理。
键重映射(Key Remapping):用 as 子句改写属性名
除了变换属性值和修饰符,映射类型还可以改写属性名本身。[P in keyof T as 新键表达式]中的as子句允许你对每个键做二次变换,通常与模板字面量类型配合使用。类型系统探索章节 给出了一个非常典型的键重映射示例:
type J<Type> = { [Property in keyof Type as `prefix_${string & Property}`]: () => Type[Property]; }; type X = { a: string; b: number; }; type Y = J<X>;这里的关键语法点:
as之后的模板字面量`prefix_${string & Property}`为每个键拼接prefix_前缀;string & Property的作用是把Property收窄到string子类型,确保它能安全地插值进模板字符串(因为键也可能包含symbol或number类型的索引签名键);- 值类型
() => Type[Property]把每个原始属性的值包装成一个返回该值的函数。
展开后Y等价于:
type Y = { prefix_a: () => string; prefix_b: () => number; };这是映射类型最强大的形态:键、值、修饰符三者皆可编程化变换。键重映射的典型应用场景包括:
- 给 API 响应字段统一加前缀(如
api_、db_); - 把
snake_case键改写为camelCase键; - 根据键名过滤掉私有字段(
as表达式返回never即剔除该键)。
与模板联合类型结合:在类型系统中生成字符串字面量
键重映射中出现的模板字面量类型,本身也是类型系统的"文本运算"能力。模板联合类型章节 展示了如何在类型层面组合字符串:
type Status = 'active' | 'inactive'; type Products = 'p1' | 'p2'; type ProductId = `id-${Products}-${Status}`; // 展开结果:"id-p1-active" | "id-p1-inactive" | "id-p2-active" | "id-p2-inactive"当模板字面量中嵌入的是联合类型时,TypeScript 会自动做笛卡尔积展开,生成全部组合。将这一能力嵌入映射类型的as子句,就可以在编译期批量生成"规范化键名",例如为每个状态机状态生成带前缀的 action type:
type Action = 'create' | 'update' | 'delete'; type Entity = 'user' | 'order'; type ActionType = `entity/${Entity}/${Action}`; // "entity/user/create" | "entity/user/update" | ... 共 6 种组合 type Actions<Type> = { [K in keyof Type as `on${Capitalize<string & K>}`]: () => void; };后者借助内置的Capitalize<T>工具类型(见 类型操作章节 中的Capitalize、Uppercase、Lowercase、Uncapitalize系列),把click变成onClick,是事件回调对象映射的常见写法。
映射类型是内置工具类型的实现根基
理解映射类型后,会发现 TypeScript 内置的众多工具类型本质上就是一组预置好的映射。类型操作章节 的 Utility Types 列表可以直接用映射类型的眼光重新审视:
Partial<T>:即{ [P in keyof T]?: T[P] }—— 全部属性可选;Required<T>:即{ [P in keyof T]-?: T[P] }—— 全部属性必填;Readonly<T>:即{ readonly [P in keyof T]: T[P] }—— 全部属性只读;Record<K, T>:用联合键K构建一个所有值都是T的对象类型。
Partial<Person>的展开结果在章节中有明确标注:
type Person = { name: string; age: number; }; type A = Partial<Person>; // { name?: string | undefined; age?: number | undefined; }Readonly<T>与Record<K, T>的示例:
type Person = { name: string; age: number; }; type A = Readonly<Person>; const a: A = { name: 'Simon', age: 17 }; a.name = 'John'; // Invalid:只读属性不允许重新赋值type Product = { name: string; price: number; }; const products: Record<string, Product> = { apple: { name: 'Apple', price: 0.5 }, banana: { name: 'Banana', price: 0.25 }, }; console.log(products.apple); // { name: 'Apple', price: 0.5 }因此,当你在项目中看到Partial、Readonly、Required时,其实正在使用映射类型;而当你需要比内置工具更定制化的变换时,直接手写[P in keyof T]即可获得同等甚至更强的表达能力。
映射类型与类型操作体系的定位
映射类型并非孤立存在,它是 TypeScript"从类型创建类型"这一更大体系中的一环。类型操作章节 将类型构造方式归纳为四类:
- 交集类型(
&):合并多个类型的成员; - 联合类型(
|):类型可以是若干候选之一; - 映射类型:遍历并变换现有类型的属性;
- 条件类型(
extends ? :):根据条件在类型之间选择,常配合infer提取子类型。
例如其中的ExtractParam<T>通过条件类型提取函数参数类型:
type ExtractParam<T> = T extends (param: infer P) => any ? P : never; type MyFunction = (name: string) => number; type ParamType = ExtractParam<MyFunction>; // string在项目实践中,映射类型常与条件类型、模板字面量类型协同作战——映射负责"遍历和变换",条件类型负责"分支决策",模板字面量负责"键名与字符串的生成"。
实战场景:用映射类型解决真实问题
场景一:将实体类型转换为表单校验模型
type User = { name: string; email: string; age: number; }; // 每个字段变为"值 + 错误信息"的结构 type FieldState<T> = { [K in keyof T]: { value: T[K]; error: string | null; }; }; type UserForm = FieldState<User>; // { name: { value: string; error: string | null }; email: {...}; age: {...} }场景二:统一为接口响应追加元数据字段
type WithMeta<T, M> = { [K in keyof T]: T[K]; } & { meta: M; }; type ApiUser = WithMeta<User, { fetchedAt: string }>;场景三:将数据库行对象映射为前端可编辑表单(部分字段可选)
type Editable<T> = { [K in keyof T]?: T[K]; }; type UserDraft = Editable<User>; // 所有字段均可选,适合表单草稿这些模式共同体现了映射类型的设计哲学:类型层面的"批量操作"不需要运行时开销,全部变换发生在编译期,最终产物是精确的、可被编辑器与编译器校验的类型约束。
小结
映射类型通过keyof获取属性集合、用[P in keyof T]遍历属性、用T[P]索引访问原值类型,再辅以readonly/-readonly、?/-?修饰符与as键重映射,构成了一个完整的"类型工厂"体系。它既是Partial、Readonly、Required、Record等内置工具类型的底层实现,也是你在面对 DTO 转换、表单模型、事件回调映射、API 键名规范化等场景时最值得优先考虑的类型编程手段。
深入学习本主题,建议按以下路径阅读仓库内容:
- 映射类型(英文原版) 与 西班牙语版:掌握核心语法与示例;
- 映射类型修饰符:掌握
readonly、-readonly、?; - 索引签名:理解可映射的键类型及数字/字符串索引转换;
- 模板联合类型:结合模板字面量做键名编程;
- 类型操作总览:将映射类型放入"从类型创建类型"的完整体系,并对照全部内置工具类型。
- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
相关推荐
The Concise TypeScript Book:映射类型(Mapped Types)实战指南
The Concise TypeScript Book:映射类型(Mapped Types)实战指南 映射类型是 TypeScript 类型系统中最强大的类型变
文档教程TypeScript 映射类型(Mapped Types)实战指南——The Concise TypeScript Book 核心章节解读
TypeScript 映射类型(Mapped Types)实战指南——The Concise TypeScript Book 核心章节解读 本文依据开源书籍《T
文档教程《The Concise TypeScript Book》精读:Mapped Types 映射类型完全指南
《The Concise TypeScript Book》精读:Mapped Types 映射类型完全指南 映射类型(Mapped Types)是 TypeSc
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考