TypeScript 映射类型

映射类型

有的时候一个类型建立在另一个类型的基础之上,这时候可以考虑使用映射类型。 映射类型建立在索引签名类型的基础上,让我们先回顾下索引类型签名:

type OnlyStrAndNum = {
  [key: string]: string | number;
};

const conforms: OnlyStrAndNum = {
  name: 'zsd',
  age: 19,
} 

而映射类型,就是使用了 PropertyKeys 联合类型的泛型,其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型:

type OptionsFlags<T> = {
  [Property in keyof T]: boolean;
}

在这个例子中OptionsFlags会遍历type的所有属性,然后设置为布尔类型:

type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
}

type FeatureOptions = OptionsFlags<FeatureFlags>;
// type FeatureOptions = {
//   darkMode: boolean;
//   newUserProfile: boolean;
// }

映射修饰符

在使用映射类型时,有两个额外的修饰符可能是会用到的,一个是readonly设置属性为只读,另一个就是可选修饰符?,你可以通过写前缀加号+减号-来添加或者删除这些修饰符,如果没有写前缀,相当于使用了+前缀。

// 删除属性中的只读属性
type CreateMutable<T> = {
  - readonly [Property in keyof T]: T[Property];
}

type LockedAccount = {
  readonly id: string;
  readonly name: string;
}

type UnlockedAccount  = CreateMutable<LockedAccount>;
// type UnlockedAccount = {
//   id: string;
//   name: string;
// }
// 删除属性中的可选属性
type Concrete<T> = {
  [Property in keyof T] -?: T[Property];
}

type MaybeUser  = {
  id: string;
  name?: string;
  age?: number;
}

type User = Concrete<MaybeUser>;
// type User = {
//   id: string;
//   name: string;
//   age: number;
// }

通过as实现键名重新映射

在TypeScript4.1以后,你可以在映射类型中使用as语句对键名重新映射:

type MappedTypeWithNewProperties<T> = {
  [Property in keyof T as NewPropertyKey]: T[Property];
}

举个例子,你可以使用模板字面量类型,基于之前的属性名创建一个新属性名:

type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
  id: number;
  name: string;
  age: number;
  sex: string;
}

type LazyPerson = Getters<Person>;
// type LazyPerson = {
//   getId: () => number;
//   getName: () => string;
//   getAge: () => number;
//   getSex: () => string;
// }

你也可以使用Exclude过滤掉某一些属性:

type RemoveKindField<T> = {
  [Property in keyof T as Exclude<Property, 'kind'>]: T[Property]
};

interface Circle {
  kind: 'circle',
  radius: number;
}

type KindLessCircle = RemoveKindField<Circle>;

你可以遍历任何联合类型,不仅仅是string|number|symbol这种联合类型,可以是任意联合类型:

type EventConfig<Events extends { kind: string }> = {
  // 这里E是类型,使用索引访问类型取到kind
  [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;
// }

深入探索

映射类型也可以搭配其他的功能使用,例如我们有一个使用条件类型的映射类型,会根据对象是否有pii属性返回true或者false:

type ExtractPII<T> = {
  [Property in keyof T]: T[Property] extends { pii: boolean } ? true : false
}

type DBFields = {
  id: { format: 'incrementing' };
  name: { type: 'string'; pii: true };
}

type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
// type ObjectsNeedingGDPRDeletion = {
//   id: false;
//   name: true;
// }