第 6 章 · 映射类型
- 掌握三个「类型操作符」:
keyof、typeof、索引访问T[K] - 用映射类型
{ [P in K]: ... }批量变换对象的每个属性 - 手写 6 个常用内置工具类型:
Partial、Required、Readonly、Pick、Omit、Record - 用映射修饰符
+?/-?/+readonly/-readonly精确控制属性形态
6.1 三个操作符:keyof / typeof / 索引访问
6.1.1 keyof:取出对象的键
interface User {
name: string
age: number
}
type Keys = keyof User // 'name' | 'age'
keyof 返回的是键的联合类型,随对象结构变化:
type K1 = keyof { a: 1; b: 2 } // 'a' | 'b'
type K2 = keyof { [k: string]: any } // string | number
type K3 = keyof string[] // number | 'length' | 'push' | ...(数组的键)
keyof string[] 不是 number,而是「数组所有可用键」——number(下标)、'length'、以及所有数组方法的键。这提醒我们:keyof 是「这个类型实际有那些键」,而不是你脑补的「应该是哪些」。
6.1.2 typeof:从值反推类型
const config = { debug: true, theme: 'dark' }
type Config = typeof config
// { debug: boolean; theme: string }
在值的语境里用 typeof(它操作的是「值」),在类型的语境里用 keyof(操作的是「类型」)。它俩搭档很常见:keyof typeof config 就是「config 这个对象的键的类型」。
6.1.3 索引访问 T[K]:取某个属性的类型
interface User {
name: string
age: number
}
type Age = User['age'] // number
type Name = User['name'] // string
键是联合时,结果是联合:
type Union = User['name' | 'age'] // string | number
type All = User[keyof User] // string | number(所有属性值的联合)
对数组/元组用 T[number] 取出元素类型:
type El = (string | number)[] // string | number
type E2 = [1, 'a', true][number] // 1 | 'a' | true
这三个操作符是「类型层面做数据操作」的基础。映射类型就是「把这三者组合起来批量变换」。
6.2 映射类型:批量变换属性
6.2.1 语法:{ [P in K]: ... }
in 遍历 K(一个键的联合),对每个键生成一个属性:
type Copy<T> = {
[P in keyof T]: T[P] // 遍历 T 的每个键,值类型取 T[P]
}
Copy<User> 就得到和 User 结构一样的类型。真正的价值在「遍历时顺手改」:
// 每个属性都变成可选的
type ToOptional<T> = {
[P in keyof T]?: T[P]
}
// 每个属性都变成只读的
type ToReadonly<T> = {
readonly [P in keyof T]: T[P]
}
6.2.2 映射是「同态」的:保留结构
当映射遍历的是 keyof T 时,它会保留原类型的一些性质(比如数组/元组形态):
type Copy<T> = { [P in keyof T]: T[P] }
type Arr = Copy<[1, 2]> // 结果仍是 [1, 2](元组被保留),而不是 { 0: 1; 1: 2; length: 2 }
这个「同态映射保留数组/元组」的特性,后面做深度类型体操时很关键。
6.2.3 重映射 as:改键名
TS 4.1 起,映射还可以改键(用 as 子句):
// 给每个键加前缀
type Prefixed<T> = {
[P in keyof T as `get${Capitalize<P>}`]: () => T[P]
}
type API = Prefixed<{ user: User; list: string[] }>
// { getUser: () => User; getList: () => string[] }
as 后返回 never 的键会被剔除——这是过滤属性的常用手法(6.4 的 Omit 就用它)。
6.3 手写工具类型:Pick / Partial / Required / Readonly / Record
这五个是 TS 内置、也最常被要求手写的工具类型。学会手写它们,你才真正理解「映射类型」。
6.3.1 Pick:挑出部分键
type MyPick<T, K extends keyof T> = {
[P in K]: T[P] // 只遍历 K 中的键
}
type User = { name: string; age: number; email: string }
type Name = MyPick<User, 'name'> // { name: string }
type Base = MyPick<User, 'name' | 'age'> // { name: string; age: number }
K extends keyof T 保证只能挑「T 确实有的键」——传错键名立刻报错。
6.3.2 Partial:全部变可选
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type User = { name: string; age: number }
type PartialUser = MyPartial<User>
// { name?: string; age?: number }
6.3.3 Required:全部变必选
需要「去掉 ?」——用映射修饰符 -?:
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
type PartialUser = { name?: string; age?: number }
type FullUser = MyRequired<PartialUser>
// { name: string; age: number }
6.3.4 Readonly:全部变只读
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
6.3.5 Record:构造「键 → 值」对象
type MyRecord<K extends keyof any, V> = {
[P in K]: V
}
type Roles = MyRecord<'admin' | 'user', string>
// { admin: string; user: string }
keyof any = string | number | symbol(合法的键类型全集)。用它约束 K,意思是「K 必须是能当键的类型」。
| 写法 | 效果 |
|---|---|
? | 变成可选 |
-? | 变成必选 |
readonly | 变成只读 |
-readonly | 去掉只读 |
+readonly / +? | 显式「加上」(默认行为,可省略) |
6.4 Omit:用「重映射剔除键」实现
Omit = 「先 Pick 除去某键后的键集合」。两种写法,用 as 重映射的版本更优雅:
// 写法一:keyof + 排除(第 7 章 Exclude 的前奏)
type MyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P]
}
// 写法二:as 重映射剔除(推荐,无需 Exclude)
type MyOmit2<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
type User = { name: string; age: number; email: string }
type NoEmail = MyOmit2<User, 'email'>
// { name: string; age: number }
重映射子句 as P extends K ? never : P:当 P 是要剔除的键时,as 的结果是 never,该键就被丢弃;否则保留原键 P。这是「映射 + 过滤」的组合技。
6.5 一张图:映射类型的结构
{ [P in K as NewKey]: ValueType }
↑ ↑ ↑
遍历键 可改键 值的类型
(K) (as...never 剔除)
配合修饰符 ? / readonly 和 - 前缀,就能精确控制每个属性长什么样。
本章小结
keyof取键、typeof反推值、T[K]取属性类型——三件套是基础- 映射类型
{ [P in K]: ... }批量变换属性;同态映射保留数组/元组 as重映射可改键、可剔除键(返回never)- 手写
Pick/Partial/Required/Readonly/Record/Omit= 理解映射类型的最好方式
本章练习
挑战题
| 题号 | 题目 | 难度 | 考察点 |
|---|---|---|---|
| 00004 | 实现 Pick | easy | 遍历键的子集 |
| 00007 | 对象属性只读 | easy | readonly 映射 |
| 00003 | 实现 Omit | medium | as 重映射剔除 |
| 00011 | 元组转换为对象 | easy | T[number] + 映射 |
| 00008 | 对象部分属性只读 | medium | 交叉 + Pick 子集 |
| 00527 | Append to object | medium | 键联合 + 条件取值 |
做完
MyPick、MyOmit,再回头看第 5 章getProp的K extends keyof T——你会发现整条线是通的。
代码练习
-
手写全部五个:不用
Partial/Required/Readonly/Pick/Record,各写一个My*版本,验证行为一致。 -
键加前缀:写
WithGet<T>,把每个属性k变成getK: () => T[k](用as重映射 +Capitalize)。验证WithGet<{ user: string }>得到{ getUser: () => string }。 -
剔除指定键:用
as重映射写DropKeys<T, K>,把K中的键从T里剔除(as P extends K ? never : P)。 -
映射保持元组:写
type Same<T> = { [P in keyof T]: T[P] },验证Same<[1, 2]>还是[1, 2]而不是普通对象。 -
组合运用:用
MyPick+MyOmit实现「一个类型既有必选又有可选」的混合:给定User,产出{ name: string; age?: number; email?: string }(提示:Pick出必选部分 +Partial出可选部分,再交叉)。