Mapped types allow you to create new types based on existing ones. They provide a way to transform the properties of an existing type in a systematic way, creating a new type with modified property types, names, or modifiers.
A mapped type looks like this:
type MappedType<T> = {
[P in keyof T]: T[P];
};
Here, P in keyof T
iterates over each property P
in type T
.
You can transform property types:
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type User = { name: string; age: number };
type NullableUser = Nullable<User>;
// Equivalent to: { name: string | null; age: number | null; }
You can add or remove readonly
and optional modifiers. Removing modifiers can be done using -
:
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
You can remap property keys using the as
keyword. We can use Capitalize<T>
, Lowercase<T>
, and Uppercase<T>
helper generics to manipulate the keys.
Note: This is similar to the syntax for type assertions using "as". But it is not the same concept.
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// Equivalent to: { getName: () => string; getAge: () => number; }
Another example:
type ChangeHandler<State extends Record<string, unknown>> = {
[K in (keyof State & string) as `on${Capitalize<K>}Change`]?: (value: State[K]) => void;
};
type State = {
selectedName: string;
pageNo: number;
}
type StateChangeHandler = ChangeHandler<State>;
// ^ StateChangeHandler: {
// onSelectedNameChange: (value: User) => void;
// onPageNoChange: (value: number) => void;
//}
Mapped types are extremely useful for creating reusable type transformations, reducing code duplication, and enabling more flexible and powerful type manipulations in TypeScript.
You can filter out keys by producing never via a conditional type:
// Remove the 'kind' property
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^ KindlessCircle: {
// radius: number;
// }
You can map over arbitrary unions, not just unions of string | number | symbol
, but unions of any type.
Here, we are mapping over a union of event types and creating a configuration object that maps each event type to a handler function:
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>
// ^ Config: {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
// }