一、映射类型
TypeScript 提供了从旧类型中创建新类型的一种方式——映射类型。在映射类型中,新类型以相同的形式去转换旧类型里每个属性。例如,我们可以让接口中的每个属性成为只读属性或可选属性。
interface Obj {
x: number;
y: string;
n: any
}
我们可以使用 type 来声明一个泛型接口,结合索引类型,来声明一个映射类型。
type Mapping<T> = {
[P in keyof T]: number;
}
这个映射类型的作用是产生一个新类型,新类型中的成员和接口 Obj 相同,但成员的值的类型都是 number。接着我们就可以使用映射类型来改变已有的类型。
type MappingObj = Mapping<Obj>;
let obj: MappingObj = {
x: 1,
y: 2,
n: 3
};
此时 MappingObj 就是一个由映射类型 Mapping 产生的新类型,它拥有和接口 Obj 相同的成员 x、y 和 n,但成员的值的类型都是 number。
二、一些常用的 TS 内置类型
接下来介绍一些 TS 的内置类型,可以用来作为映射类型。
1. ReadOnly 接口
如果我们想让接口 Obj 中的变为只读怎么办?有一个特别简单的方法,就是直接使用 TS 的内置类型 Readonly。
type ReadonlyObj = Readonly<Obj>;
首先我们定义了一个类型别名 ReadonlyObj,值为 TS 内置的类型 Readonly,传入的值是 Obj。ReadonlyObj 的类型和 Obj 是一样的,只是所有成员都变成了只读。
那这种内置的接口是如何实现的呢?我们来看下相关的源码:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
我们来看下 Readonly 的实现,首先这是一个泛型接口,而且是一个有索引类型的泛型接口。它的索引签名是 "P in keyof T",其中 keyof T 就是一个索引类型的查询操作符,它表示 T 的所有属性的联合类型("x | y | n" 这种形式的属性列表)。这里的 P in 相当于 for in 操作,类型变量 P 会依次绑定到每个属性。
索引签名的返回值就是一个索引访问操作符了。这里的 T[P] 中 T 表示传入的对象,P 表示依次绑定的属性,T[P] 则为 P 属性的类型。最后前面加上 readonly 映射原始类型的所有属性,就把所有的属性变成了只读。
type Readonly<对象> = {
readonly 属性列表[0]: 结果类型;
readonly 属性列表[1]: 结果类型;
readonly 属性列表[2]: 结果类型;
}
以上就是内置接口 Readonly 的实现了。
2. Partial 类型
如果我们想把一个接口的属性都变成可选的怎么办?
type PartialObj = Partial<Obj>;
使用内置的 Partial 类型,这样新的类型就能把成员变成可选。
源码如下:
type Partial<T> = {
[P in keyof T]?: T[P] | undefined;
}
这个类型和刚刚的 Readonly 类型的实现几乎是一样的,只不过加上了 "?" 把属性变成了可选。
3. Pick 类型
Pick 类型可以抽取 obj 的一些子集,它接收两个参数,第一个参数就是接口 Obj,第二个参数就是我们要抽取的属性 key。
type PickObj = Pick<Obj, 'x' | 'y'>;
这样接口的 x 和 y 成员就能被单独抽取出来,形成一个新的类型。
源码实现:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
第一个参数 T 表示我们要抽取的对象,第二个参数是 K,有个约束就是,K 一定要是来自变量 T 属性字面量的联合类型。
4. Record 类型
Record 类型创建了一个拥有 keys 类型的属性和对应值的 Type 的对象。
interface Obj {
x: number,
y: string,
n: any
}
type RecordObj = Record<'a' | 'b', Obj>;
let obj: RecordObj = {
a: { x: 1, y: '1', n: 2},
b: { x: 2, y: '3', n: 4}
};
这里我们需要预定义一些新的属性 a 和 b,第二个参数是来自一个我们已知的类型。这样新的类型就有一些属性由 Record 第一个参数指定,类型由 Record 第二个参数指定。这种类型就是一种非同态的类型。
可以看到 Record 类型的好处是简明的。当我们想要去限制属性时,也就是 Record 类型大显身手的时候。下面的示例是我们在 Record 中使用字面量的联合类型去限制属性键。
type Roles = 'tester' | 'developer' | 'manager';
const staffCount: Record<Roles, number> = {
tester: 10,
developer: 20,
manager: 1
};
在示例中,我们使用联合类型约束定义了一个类型。如果我们尝试去访问一个不在联合类型中的属性时,集成开发环境就会进行提示。当我们维护一个复杂类型的时候这非常有用,因为编译器会阻止这类错误的发生。Record 接口另一个有用的功能是 keys 可以是枚举。在下面的例子中,我们使用 StaffTypes 枚举作为 Record 类型的限制词,因此可读性更好。
enum StaffTypes {
tester = 'tester',
developer = 'developer',
manager = 'manager'
}
const staffCount: Record<StaffTypes, number> = {
tester: 10,
developer: 20,
manager: 1
};
请注意,在 TypeScript 2.9后才支持枚举。因此,在2.9版本之前,key 的类型被限制为 string 类型。
Record 类型还可以和 key of 组合。通过使用 key of 从现有类型中获取所有的属性,并和 string 组合,我们可以做如下事情:
interface Staff {
name: string,
salary: number
}
type staffJson = Record<keyof Staff, string>;
const product: staffJson = {
name: 'John',
salary: '3000'
}
当我们想要保留现有类型的属性但将值类型转换为其他类型时,这很便捷。
Record 类型的源码实现如下:
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
}
K entends string | number | symbol 约束 K 必须为 string、number、symbol 类型或联合类型,每个属性 P in K,都转换为 T 类型。
总结:映射类型本质上是一种预设类型的泛型接口,通常还会集合到索引类型获取对象的属性和属性值,从而把一个对象变成我们想要的结构。