TS - 映射类型(Mapped Types)、in
简单的例子
以下是一个简单的例子,通过索引访问类型(Indexed Access Types),可以给一个对象定义 key 的类型以及 value 的类型。
type Horse = {
age: number;
};
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
迭代联合类型
可以提供一个联合类型给索引访问类型,通过 in
关键字迭代这个联合类型并创建 key 的类型和 value 的类型:
file:[in 迭代 T 泛型的 key]
type OptionsFlags<T> = {
[P in keyof T]: boolean;
};
[P in keyof T]
通过关键字 keyof
获取对象类型的所有 key,这些 key 组合成一个联合类型,然后通过 in
迭代联合类型,并创建 OptionsFlags 的所有 key,并且这些 key 的值类型都是 boolean。
file:[使用 OptionsFlags]
type Features = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<Features>;
// ^?
//type FeatureOptions = {
// darkMode: boolean;
// newUserProfile: boolean;
//}
从以上实例中得知,最终我们得到了一个新的类型 FeatureOptions,它具有与 Features 类型相同的属性,但其值都被更改为 boolean 类型。
映射修改器(Mapping Modifiers)
修改 readonly
我们可以移除一个类型的 readonly
关键字,让它变得可读可写的状态。如下所示,创建一个 CreateMutable 类型别名,删除 readonly 关键字,就在其前面添加一个 -
,反之,添加 readonly 关键字,就在其前面添加一个 +
:
// 移除 readonly
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
// ^?
//type LockedAccount = {
// readonly id: string;
// readonly name: string;
//}
type UnlockedAccount = CreateMutable<LockedAccount>;
// ^?
//type UnlockedAccount = {
// id: string;
// name: string;
//}
LockedAccount 类型别名中的所有字段都是 readonly
,把这个类型别名传递给 CreateMutable 类型别名之后,通过映射修改器删除所有字段的 readonly
,得到 UnlockedAccount,最终如上所示,已经没有了 readonly
关键字。
修改可选属性
它也可以修改对象类型的可选属性,删除可选就在 ?
前添加一个 -
,反之,添加可选就在属性前添加一个 +?
。具体如下所示:
// 移除可选属性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
// ^?
//type User = {
// id: string;
// name: string;
// age: number;
//}
键重新映射(Key Remapping)
在 TS 4.1 版本之后,我们可以通过 as
关键字对键重新映射,改变它的类型。
新的属性名
通过模板文字类型修改键的名称,具体如下所示:
file:[生成 getters 属性]
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
};
获取 T 对象类型的所有键名的联合类型。P 代表一个个键名,由 in
迭代联合类型而来。as
关键字对前面进行重命名操作,重命名的结果是后面模板文字类型。
Capitalize 类型别名是 TS 内置的工具类型,它把一个单词的首字母转换为大写字母。由于这个类型别名工具的类型参数需要一个 string 类型的,而 P 是一个 string | number | symbol
类型,因此,string & P
的结果是 string。
T[P]
就是取联合类型中的类型元素,其实我们可以把联合类型看作是一个数组(或者集合),取值就通过名称来取。
tip:[start]交叉运算符 &
在合并类型过程中,如果一个类型包含了其他类型的子集,那么结果类型将取该子集类型。
类型 string | number | symbol
是一个联合类型,表示一个值可以是 string、number 或 symbol 中的任意一种类型。
当我们使用交叉运算符 & 将这两个类型进行交叉操作时,由于 string 类型是 string | number | symbol
的子集,交叉类型的结果将取 string 类型。
type R = (string | number | symbol) & string;
// ^? string
tip:[end]
以下是使用 Getters 类型别名的结果,它会把我们的 Person 接口下的所有属性名称转换为 getXxx 的形式,并且值的类型是一个函数类型,函数的返回值类型是由属性的类型决定的:
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// ^?
//type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
//}
过滤属性
P 如果是 kind,就返回 never,移除本次迭代,保留非 kind 字段名。
type RemoveKindField<T> = {
[P in keyof T as Exclude<P, "kind">]: T[P]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^? type KindlessCircle = { radius: number; }
复杂联合类型
除了对基础联合类型,比如 string | number | symbol
这样以外的操作以外,还可以操作复杂一点的类型。
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>
// ^?
//type Config = {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
//}
Events 扩展一个对象类型,意思是说,对联合类型中,扩展每一个元素的属性,如果不这样做,在 E["kind"]
的时候会报错,因为不知道这个 E 会有 kind 属性。然后,在索引访问类型中迭代 Events 时进行键重新映射(Key Remapping)操作。
tip:[start]在这里 Events 是一个联合类型,这里和最开始的 迭代联合类型 小节是一样的,迭代的是联合类型,而不是迭代某个对象类型中的键组成的联合类型,那个是 E in keyof Event
之类的。tip:[end]
假如,kind 名字是 "square",那么就是 square: (event: SquareEvent) => void
,如上所示。
属性值的条件类型
属性值还可以使用条件类型来决定本次迭代的值类型是什么,如下所示:
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};