TypeScript 类型映射
简介
映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。例如从已有的对象类型 A 映射得到新的对象类型 B:
ts
type A = {
foo: number;
bar: number;
};
type B = {
[prop in keyof A]: string;
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
实例中 [prop in keyof A]
得到了类型 A
的所有属性名,然后将每个属性名对应的属性值类型改为了 string
。如果要得到属性在原对象中的类型,可以使用方括号语法:
ts
type A = {
foo: number;
bar: string;
};
type B = {
[prop in keyof A]: A[prop];
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
不使用联合类型,直接使用某种具体类型进行属性名映射,也是可以的:
ts
type MyObj = {
[p in "foo"]: number;
};
// 等同于
type MyObj = {
foo: number;
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
甚至还可以写成 p in string
:
ts
type MyObj = {
[p in string]: boolean;
};
// 等同于
type MyObj = {
[p: string]: boolean;
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
映射修饰符
通过映射,可以把某个对象的所有属性改成可选属性或只读属性:
ts
type A = {
a: string;
b: number;
};
type B = {
readonly [Prop in keyof A]?: A[Prop];
};
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
如果要删除可选或只读特性,可以使用 TS 的映射修饰符:
+
:写成+?
或+readonly
,为映射属性添加?
修饰符或readonly
修饰符。-
:写成-?
或-readonly
,为映射属性移除?
修饰符或readonly
修饰符。
ts
// 增加
type MyObj<T> = {
+readonly [P in keyof T]+?: T[P];
};
// 移除
type MyObj<T> = {
-readonly [P in keyof T]-?: T[P];
};
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
注意
–?
修饰符移除了可选属性以后,该属性就不能等于 undefined
了,实际变成必选属性了。但是,这个修饰符不会移除 null
类型
另外,+?
修饰符可以简写成 ?
,+readonly
修饰符可以简写成 readonly
。
键名重映射
TS 4.1 引入了键名重映射,允许改变键名:
ts
type A = {
foo: number;
bar: number;
};
type B = {
[p in keyof A as `${p}ID`]: number;
};
// 等同于
// type B = {
// fooID: number;
// barID: number;
// };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
下面是另一个例子:
ts
interface Person {
name: string;
age: number;
location: string;
}
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
get
:为键名添加的前缀。Capitalize<T>
:一个原生的工具泛型,用来将T
的首字母变成大写。string & P
:一个交叉类型,其中的P
是keyof
运算符返回的键名联合类型string|number|symbol
,但是Capitalize<T>
只能接受字符串作为类型参数,因此string & P
只返回P
的字符串属性名。
键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性:
ts
type User = {
name: string;
age: number;
};
type Filter<T> = {
[K in keyof T as T[K] extends string ? K : never]: string;
};
type FilteredUser = Filter<User>; // { name: string }
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
上面示例中,映射 K in keyof T
获取类型 T 的每一个属性以后,然后使用 as Type
修改键名。
它的键名重映射 as T[K] extends string ? K : never]
,使用了条件运算符。如果属性值 T[K]
的类型是字符串,那么属性名不变,否则属性名类型改为 never
,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性。
由于键名重映射可以修改键名类型,所以原始键名的类型不必是 string|number|symbol
,任意的联合类型都可以用来进行键名重映射:
ts
type S = {
kind: "square";
x: number;
y: number;
};
type C = {
kind: "circle";
radius: number;
};
type MyEvents<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
};
type Config = MyEvents<S | C>;
// 等同于
type Config = {
square: (event: S) => void;
circle: (event: C) => void;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21