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 PropertyKey
s (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;
}