跳到主要内容

第 6 章 · 映射类型

读完本章你会
  • 掌握三个「类型操作符」:keyoftypeof、索引访问 T[K]
  • 用映射类型 { [P in K]: ... } 批量变换对象的每个属性
  • 手写 6 个常用内置工具类型:PartialRequiredReadonlyPickOmitRecord
  • 用映射修饰符 +? / -? / +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 是什么

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

的语境里用 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 是什么

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 返回 never = 剔除

重映射子句 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实现 Pickeasy遍历键的子集
00007对象属性只读easyreadonly 映射
00003实现 Omitmediumas 重映射剔除
00011元组转换为对象easyT[number] + 映射
00008对象部分属性只读medium交叉 + Pick 子集
00527Append to objectmedium键联合 + 条件取值

做完 MyPickMyOmit,再回头看第 5 章 getPropK extends keyof T——你会发现整条线是通的。

代码练习

  1. 手写全部五个:不用 Partial/Required/Readonly/Pick/Record,各写一个 My* 版本,验证行为一致。

  2. 键加前缀:写 WithGet<T>,把每个属性 k 变成 getK: () => T[k](用 as 重映射 + Capitalize)。验证 WithGet<{ user: string }> 得到 { getUser: () => string }

  3. 剔除指定键:用 as 重映射写 DropKeys<T, K>,把 K 中的键从 T 里剔除(as P extends K ? never : P)。

  4. 映射保持元组:写 type Same<T> = { [P in keyof T]: T[P] },验证 Same<[1, 2]> 还是 [1, 2] 而不是普通对象。

  5. 组合运用:用 MyPick + MyOmit 实现「一个类型既有必选又有可选」的混合:给定 User,产出 { name: string; age?: number; email?: string }(提示:Pick 出必选部分 + Partial 出可选部分,再交叉)。