When you don’t want to repeat yourself, sometimes a type needs to be based on another type.
Mapped types build on the syntax for index signatures, which are used to declare the types of properties which has not been declared ahead of time:
type
TryOnlyBoolsAndHorses = { [key : string]: boolean |Horse ; }; constconforms :OnlyBoolsAndHorses = {del : true,rodney : false, };
A mapped type is a generic type which uses a union created via a keyof
to iterate through the keys of one type to create another:
type
TryOptionsFlags <Type > = { [Property in keyofType ]: boolean; };
In this example, OptionFlags
will take all the properties from the type Type
and change their values to be a boolean.
type
TryFeatureFlags = {darkMode : () => void;newUserProfile : () => void; }; typeFeatureOptions =OptionsFlags <FeatureFlags >; // ^ = type FeatureOptions = { // darkMode: boolean; // newUserProfile: boolean; // }
Mapping Modifiers
There are a two additional modifiers which can be applied during mapping: readonly
and ?
which affect mutability and optionality respectively. Both of these modifiers support a prefix of -
or +
with +
being the default.
tstype CreateMutable<Type> = { -readonly [Property in keyof Type]: Type[Property]; }; type LockedAccount = { readonly id: string; readonly name: string; }; type UnlockedAccount = CreateMutable<LockedAccount>; // ^?
type
TryConcrete <Type > = { [Property in keyofType ]-?: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:
tstype MappedTypeWithNewProperties<Type> = { [Properties in keyof Type as NewKeyType]: Type[Properties] }
You can leverage features like template literal types to create new property names from prior ones:
type
TryGetters <Type > = { [Property in keyofType as `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 type
TryRemoveKindField <T > = { [K in keyofT asExclude <K , "kind">]:T [K ] }; interfaceCircle {kind : "circle";radius : number; } typeKindlessCircle =RemoveKindField <Circle >; // ^ = type KindlessCircle = { // radius: number; // }
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
:
tstype ExtractPII<Type> = { [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false; }; type DBFields = { id: { format: "incrementing" }; name: { type: string; pii: true }; }; type ObjectsNeedingGDPRDeletion = CreateMutable<DBFields>;