阮一峰《TypeScript 教程》学习笔记——类型映射
1. 一段话总结
TypeScript 的类型映射是将一种类型按规则转换为另一种类型的核心机制,主要用于对象类型,核心语法为 [prop in keyof Type]: TargetType(通过keyof提取原类型键名,in遍历键名联合类型);支持通过映射修饰符(+?/-?控制可选属性、+readonly/-readonly控制只读属性)调整属性的可选/只读特性;TypeScript 4.1 引入的键名重映射(as子句,如[p in keyof A as ${p}ID])可修改键名,还能实现属性过滤(通过条件类型返回never排除属性);此外,类型映射可封装为泛型提高复用性,且与Partial<T>/Readonly<T>/Required<T>等内置工具类型直接关联,广泛用于属性类型批量修改、特性调整及键名优化。
2. 思维导图

3. 详细总结
一、类型映射简介
TypeScript 类型映射的核心是“按预设规则将源类型转换为目标类型”,主要针对对象类型,解决批量处理属性类型或特性的问题,避免重复编写相似类型。
1. 基础语法与核心逻辑
| 语法组成 | 作用 | 示例 |
|---|---|---|
prop | 属性名变量,名称可自定义(如P、Key) | - |
in | 遍历运算符,逐个取出右侧联合类型的成员 | P in keyof A(遍历A的键名) |
keyof Type | 提取源类型Type的所有键名,生成联合类型(如keyof {a:number} → 'a') | `keyof {foo:number; bar:string} → ‘foo’ |
TargetType | 目标属性类型,可固定(如string)或关联源类型(如Type[P]) | [P in keyof A]: A[P](复制源类型) |
示例1:基础类型转换
将源类型A的所有属性转为string类型:
type A = { foo: number; bar: number };
type B = { [P in keyof A]: string }; // { foo: string; bar: string }
示例2:复制源类型
通过Type[P]关联源属性类型,实现类型复制:
type A = { foo: number; bar: string };
type B = { [P in keyof A]: A[P] }; // 与A完全一致
2. 泛型封装(提高复用性)
将映射逻辑封装为泛型,可复用至任意对象类型:
// 泛型:将任意类型的所有属性转为boolean
type ToBoolean<Type> = { [P in keyof Type]: boolean };
// 用法:将User的所有属性转为boolean
type User = { name: string; age: number };
type UserBoolean = ToBoolean<User>; // { name: boolean; age: boolean }
二、映射修饰符
映射会默认保留源类型的“可选(?)”和“只读(readonly)”特性,若需调整,需使用映射修饰符(+添加特性,-移除特性)。
1. 修饰符分类与用法
| 修饰符类型 | 语法 | 作用 | 示例(基于type A = { a: string; b: number }) |
|---|---|---|---|
| 添加可选属性 | +?(简写为?) | 为所有属性添加?,转为可选属性 | type OptionalA = { [P in keyof A]?: A[P] } → { a?: string; b?: number } |
| 移除可选属性 | -? | 移除所有属性的?,转为必选属性(对应内置Required<T>) | type ConcreteA = { [P in keyof A]-?: A[P] } → { a: string; b: number } |
| 添加只读属性 | +readonly(简写为readonly) | 为所有属性添加readonly,转为只读属性(对应内置Readonly<T>) | type ReadonlyA = { readonly [P in keyof A]: A[P] } → { readonly a: string; readonly b: number } |
| 移除只读属性 | -readonly | 移除所有属性的readonly,转为可写属性 | type MutableA = { -readonly [P in keyof A]: A[P] } → { a: string; b: number } |
2. 组合使用修饰符
同时调整“只读”和“可选”特性:
// 为所有属性添加readonly和可选
type ReadonlyOptional<T> = { +readonly [P in keyof T]+?: T[P] };
// 移除所有属性的readonly和可选
type MutableConcrete<T> = { -readonly [P in keyof T]-?: T[P] };
三、键名重映射(TypeScript 4.1+)
键名重映射通过as子句修改源类型的键名,支持模板字符串、条件类型等,还可实现属性过滤。
1. 核心语法
type 目标类型 = {[P in keyof 源类型 as 新键名类型]: 目标属性类型;
};
- 新键名类型:常用模板字符串(如
${P}ID)、条件类型(如T[P] extends string ? P : never)。
2. 常见场景示例
| 场景 | 代码示例 | 结果类型 |
|---|---|---|
| 键名添加后缀 | type A = { foo: number; bar: number }; type B = { [P in keyof A as ${P}ID]: number } | { fooID: number; barID: number } |
| 键名添加前缀并大写 | type Person = { name: string; age: number }; type Getters<T> = { [P in keyof T as get${Capitalize<string & P>}]: () => T[P] } | { getName: ()=>string; getAge: ()=>number } |
| 属性过滤(保留字符串属性) | type User = { name: string; age: number }; type FilterString<T> = { [K in keyof T as T[K] extends string ? K : never]: string } | { name: string } |
3. 关键逻辑:属性过滤
通过条件类型,若属性不满足要求则返回never(表示该属性不存在),实现过滤:
type FilterNumber<T> = {// 仅保留属性值为number的属性,否则键名转为never(过滤)[K in keyof T as T[K] extends number ? K : never]: number;
};
type Data = { a: number; b: string; c: number };
type NumberData = FilterNumber<Data>; // { a: number; c: number }
四、联合类型的映射
键名重映射支持基于“非键名联合类型”(如对象联合类型)生成新类型,核心是从联合类型成员中提取字段作为新键名。
示例:基于含kind字段的对象联合类型生成函数类型对象
// 源联合类型:含kind字段的对象类型
type Square = { kind: 'square'; x: number; y: number };
type Circle = { kind: 'circle'; radius: number };// 映射逻辑:以kind字段值为键名,生成函数类型
type EventHandlers<Events extends { kind: string }> = {[E in Events as E['kind']]: (event: E) => void;
};// 生成目标类型
type ShapeHandlers = EventHandlers<Square | Circle>;
// 结果:{ square: (event:Square)=>void; circle: (event:Circle)=>void }
五、与内置工具类型的关联
TypeScript 内置工具类型多基于类型映射实现,核心对应关系如下:
| 内置工具类型 | 实现逻辑(简化版) | 作用 |
|---|---|---|
Partial<T> | { [P in keyof T]?: T[P] } | 将所有属性转为可选 |
Readonly<T> | { readonly [P in keyof T]: T[P] } | 将所有属性转为只读 |
Required<T> | { [P in keyof T]-?: T[P] } | 移除所有属性的可选特性,转为必选 |
4. 关键问题
问题1:TypeScript 类型映射的基础语法是什么?其核心作用的解决了什么开发痛点?
答案:
- 基础语法:类型映射的核心语法为
[prop in keyof Type]: TargetType,其中:keyof Type:提取源类型Type的所有键名,生成联合类型;in:遍历该联合类型,逐个获取键名并赋值给变量prop;TargetType:指定映射后属性的目标类型(可固定或关联源类型,如Type[prop])。
- 解决的痛点:
开发中若需批量处理对象类型的属性(如将所有属性转为boolean、统一添加readonly),手动编写重复类型定义会导致冗余且易出错;类型映射通过“规则化转换”实现批量处理,减少重复代码,同时提高类型定义的复用性(如封装为泛型后可复用至任意对象类型)。 - 示例:
批量将User类型的所有属性转为string:type User = { name: string; age: number }; type UserString = { [P in keyof User]: string }; // { name: string; age: string }
问题2:映射修饰符有哪几类?分别适用于哪些场景?请结合示例说明。
答案:
映射修饰符分为可选属性修饰符和只读属性修饰符,核心用于调整属性的“可选”或“只读”特性,共4种常用类型:
| 修饰符类别 | 具体修饰符 | 语法示例 | 适用场景 |
|---|---|---|---|
| 可选属性修饰符 | 添加可选 | [P in keyof T]+?: T[P](简写?) | 需将对象所有属性转为可选(如表单初始值类型) |
| 可选属性修饰符 | 移除可选 | [P in keyof T]-?: T[P] | 需将对象所有可选属性转为必选(如校验后的数据类型) |
| 只读属性修饰符 | 添加只读 | +readonly [P in keyof T]: T[P](简写readonly) | 需禁止修改对象属性(如配置项类型) |
| 只读属性修饰符 | 移除只读 | -readonly [P in keyof T]: T[P] | 需解除属性的只读限制(如动态更新的数据类型) |
示例1:添加可选属性
将Config类型的所有属性转为可选,用于表单初始值:
type Config = { theme: string; fontSize: number };
type OptionalConfig = { [P in keyof Config]?: Config[P] };
const initConfig: OptionalConfig = { theme: 'light' }; // 允许省略fontSize
示例2:移除可选属性
将OptionalConfig的可选属性转为必选,用于校验后的确定数据:
type RequiredConfig = { [P in keyof OptionalConfig]-?: OptionalConfig[P] };
const finalConfig: RequiredConfig = { theme: 'light', fontSize: 16 }; // 必须包含所有属性
问题3:键名重映射的语法是什么?如何通过它实现“属性过滤”?请结合示例说明原理。
答案:
-
键名重映射语法:TypeScript 4.1+ 支持通过
as子句修改键名,语法为:type 目标类型 = { [P in keyof 源类型 as 新键名类型]: 目标属性类型 };其中“新键名类型”可通过模板字符串(如
${P}ID)、条件类型等生成,核心是通过as子句重新定义键名。 -
属性过滤原理:
利用“条件类型返回never表示属性不存在”的特性,在as子句中添加条件判断:若属性满足要求,则保留原键名;若不满足,则返回never(过滤该属性)。 -
示例:过滤非函数类型的属性
从Action类型中仅保留“属性值为函数”的属性:// 源类型:含函数和非函数属性 type Action = {add: (x: number, y: number) => number;name: string;subtract: (x: number, y: number) => number;age: number; };// 映射逻辑:仅保留函数类型属性 type FilterFunction<T> = {[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K]; };// 过滤结果:仅保留add和subtract type FunctionAction = FilterFunction<Action>; // 结果类型:{ add: (x:number,y:number)=>number; subtract: (x:number,y:number)=>number }原理:对每个键名
K,判断T[K]是否为函数类型;若是,保留键名K;若不是,返回never(该属性被过滤)。
