Skip to content

映射类型

映射类型是类型层唯一的"循环"。类型层没有 for,要遍历键只能靠它。

基本形式

ts
type 
Mapped
<
T
> = {
[
K
in keyof
T
]:
T
[
K
]
} interface Todo {
title
: string
done
: boolean
} type
A
=
Mapped
<Todo>

[K in keyof T] 读作 for (const K of Object.keys(T))

修饰符:增删 readonly 与 ?

ts
type 
Partial
<
T
> = { [
K
in keyof
T
]?:
T
[
K
] }
type
Required
<
T
> = { [
K
in keyof
T
]-?:
T
[
K
] }
type
Readonly
<
T
> = { readonly [
K
in keyof
T
]:
T
[
K
] }
type
Mutable
<
T
> = { -readonly [
K
in keyof
T
]:
T
[
K
] }

+ 是默认的(省略即可),- 表示移除。

修饰符是保留的,不是覆盖的:

ts
interface Mixed {
  
a
?: string
b
: string
} type
P
=
Partial
<Mixed>

ab 都变成可选,a 不会"变回必填"。

键重映射(TS 4.1)

as 改写键名:

ts
type 
Getters
<
T
> = {
[
K
in keyof
T
as `get${
Capitalize
<
K
& string>}`]: () =>
T
[
K
]
} interface Todo {
title
: string
done
: boolean
} type
G
=
Getters
<Todo>

注意 K & stringkeyof T 可能是 string | number | symbol,而 Capitalize 只吃 string

用 never 过滤键

映射成 never 的键会被丢弃:

ts
type 
OnlyStrings
<
T
> = {
[
K
in keyof
T
as
T
[
K
] extends string ?
K
: never]:
T
[
K
]
} interface Mixed {
name
: string
age
: number
email
: string
} type
S
=
OnlyStrings
<Mixed>

这是"筛选属性"的标准写法,比 Omit 灵活得多(可以按值类型筛,不只是按键名)。

遍历数组与元组

映射类型用在元组上会保持长度

ts
type 
ToPromise
<
T
extends unknown[]> = {
[
I
in keyof
T
]:
Promise
<
T
[
I
]>
} type
A
=
ToPromise
<[string, number]>
type
B
=
ToPromise
<string[]>

因为元组的 keyof 包含数字索引,映射后仍是元组。

递归映射

处理嵌套结构要自己递归:

ts
type 
DeepReadonly
<
T
> =
T
extends (infer
U
)[]
?
DeepReadonly
<
U
>[]
:
T
extends Function
?
T
:
T
extends object
? { readonly [
K
in keyof
T
]:
DeepReadonly
<
T
[
K
]> }
:
T
interface Nested {
a
: string
b
: {
c
: number;
d
: {
e
: boolean } }
} type
R
=
DeepReadonly
<Nested>

三个判断的顺序很重要:先排除数组,再排除函数,最后才是普通对象。因为函数和数组都是 object 的子类型。

常见工具的实现

ts
// 官方内置的四个
type 
MyPartial
<
T
> = { [
K
in keyof
T
]?:
T
[
K
] }
type
MyRequired
<
T
> = { [
K
in keyof
T
]-?:
T
[
K
] }
type
MyReadonly
<
T
> = { readonly [
K
in keyof
T
]:
T
[
K
] }
type
MyPick
<
T
,
K
extends keyof
T
> = { [
P
in
K
]:
T
[
P
] }
// 社区常用但官方没有的 type
MyMutable
<
T
> = { -readonly [
K
in keyof
T
]:
T
[
K
] }
type
Nullable
<
T
> = { [
K
in keyof
T
]:
T
[
K
] | null }
type
Stringify
<
T
> = { [
K
in keyof
T
]: string }

Omit 不是映射类型直接实现的,它是 Pick + Exclude

ts
type 
MyOmit
<
T
,
K
extends keyof any> =
Pick
<
T
,
Exclude
<keyof
T
,
K
>>

坑与限制

1. 键重映射里不能用索引签名

ts
// 想给所有键加前缀,但保留索引签名是做不到的
type 
Prefix
<
T
> = {
[
K
in keyof
T
as `x_${
K
& string}`]:
T
[
K
]
}

2. 复杂映射会拖慢编译

嵌套映射 + 递归 + 条件类型叠在一起时,tsc 会明显变慢。见编译性能

3. 映射丢掉泛型约束信息

ts
type 
Id
<
T
> = { [
K
in keyof
T
]:
T
[
K
] }

这个 Id 看似恒等,实际上会把交叉类型拍平——它常被用作"美化类型"的工具(Prettify):

ts
type 
Prettify
<
T
> = { [
K
in keyof
T
]:
T
[
K
] } & {}
type
P
=
Prettify
<{
a
: string } & {
b
: number }>

下一步

基于 VitePress 与 Twoslash 构建