Mapped Types
#code/C4TwDgpgBAEg9gJwM7QLxQN4F8DcAoAegKgFoyBjAV2DJL1EigHkA7AGxACE442kBBFgBN4yCEijoMeKFADaAawggAXFCTAEASxYBzALpqARjzYQAhiygAfWIhT5cePOTgsNUVywBmiALZIaqwc3LwCwqIoElIyUEIQbGqalBAANLEIcEIsymre5nxpeLhAA)
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
constconforms: OnlyBoolsAndHorses = {
del:true,
rodney:false,
};A mapped type is a generic type which uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type:
type OptionsFlags<Type> = {
[PropertyinkeyofType]: boolean;
};In this example, OptionsFlags will take all the properties from the type Type and change their values to be a boolean.
type Features = {
darkMode: () =>void;
newUserProfile: () =>void;
};
typeFeatureOptions = OptionsFlags<Features>;
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}Mapping Modifiers
There are two additional modifiers which can be applied during mapping: readonly and ? which affect mutability and optionality respectively.
You can remove or add these modifiers by prefixing with - or +. If you don’t add a prefix, then + is assumed.
// Removes 'readonly' attributes from a type's properties
typeCreateMutable<Type> = {
-readonly [PropertyinkeyofType]: Type[Property];
};
typeLockedAccount = {
readonlyid: string;
readonlyname: string;
};
typeUnlockedAccount = CreateMutable<LockedAccount>;
type UnlockedAccount = {
id: string;
name: string;
}// Removes 'optional' attributes from a type's properties
typeConcrete<Type> = {
[PropertyinkeyofType]-?: Type[Property];
};
typeMaybeUser = {
id: string;
name?: string;
age?: number;
};
typeUser = Concrete<MaybeUser>;
type User = {
id: string;
name: string;
age: number;
}Key Remapping via as
In TypeScript 4.1 and onwards, you can re-map keys in mapped types with an as clause in a mapped type:
type MappedTypeWithNewProperties<Type> = {
[PropertiesinkeyofTypeasNewKeyType]: Type[Properties]
}You can leverage features like template literal types to create new property names from prior ones:
type Getters<Type> = {
[PropertyinkeyofTypeas`get${Capitalize<string&Property>}`]: () =>Type[Property]
};
interfacePerson {
name: string;
age: number;
location: string;
}
typeLazyPerson = Getters<Person>;
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}You can filter out keys by producing never via a conditional type:
// Remove the 'kind' property
typeRemoveKindField<Type> = {
[PropertyinkeyofTypeasExclude<Property, "kind">]: Type[Property]
};
interfaceCircle {
kind: "circle";
radius: number;
}
typeKindlessCircle = RemoveKindField<Circle>;
type KindlessCircle = {
radius: number;
}You can map over arbitrary unions, not just unions of string | number | symbol, but unions of any type:
type EventConfig<Events extends { kind: string }> = {
[EinEventsasE["kind"]]: (event: E) =>void;
}
typeSquareEvent = { kind: "square", x: number, y: number };
typeCircleEvent = { kind: "circle", radius: number };
typeConfig = EventConfig<SquareEvent | CircleEvent>
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}Further Exploration
Mapped types work well with other features in this type manipulation section, for example here is a mapped type using a conditional type which returns either a true or false depending on whether an object has the property pii set to the literal true:
type ExtractPII<Type> = {
[PropertyinkeyofType]: Type[Property] extends { pii: true } ? true : false;
};
typeDBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
typeObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
}