Skip to main content

Mapped types

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.

Transforming property types​

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; }

Modifying property modifiers​

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];
};

Key remapping​

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.

Filtering out object keys​

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;
// }

Mapping over any union​

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;
// }

Exercise 7.1​

Implement some utility types using mapped types.

Exercise 7.2​

Implement a type-safe event system using mapped types.