Mapped types let you create new types by transforming the properties of existing types.
Let’s see this in action. Imagine you have a User type representing data from a database:
interface User {
id: number;
username: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
Now, you want to create a type for a form where all fields are optional, maybe for a partial update operation. You could write this out manually:
interface PartialUserForm {
id?: number;
username?: string;
email?: string;
createdAt?: Date;
updatedAt?: Date;
}
But this is repetitive and error-prone. If User changes, you have to remember to update PartialUserForm. Mapped types automate this.
The syntax looks like this: {[P in K]: T}.
Kis a union of property keys.Piterates over each key inK.Tis the new type for the propertyP.
To make all properties of User optional, we can use keyof User to get all its keys and then transform them. The in keyword is crucial here; it iterates over the keys.
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PartialUserForm = Partial<User>;
Here’s what’s happening:
keyof T: This extracts a union of all property names from the typeT. ForUser, this would be"id" | "username" | "email" | "createdAt" | "updatedAt".[P in keyof T]: This is the mapped type syntax. It says, "for each property namePin the unionkeyof T…"?: T[P]: "…create a new property with the nameP, make it optional (the?), and give it the typeT[P]."T[P]is a lookup type, meaning "the type of the propertyPin typeT."
So, Partial<User> becomes exactly the PartialUserForm we wrote manually, but it’s now dynamically generated from User. If User gains a new property, say isActive: boolean, Partial<User> automatically includes isActive?: boolean.
This pattern is incredibly versatile. What if you want to make all properties readonly?
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyUser = Readonly<User>;
Here, we just added the readonly modifier before the property name. ReadonlyUser would look like:
interface ReadonlyUser {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: Date;
readonly updatedAt: Date;
}
You can also transform the types of the properties themselves. Let’s say you want a type where all Date properties are replaced with string (e.g., for JSON serialization).
type StringifyDates<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P];
};
type UserAsJson = StringifyDates<User>;
This uses a conditional type within the mapped type. For each property P:
- If the type of
P(T[P]) isDate, then the new type isstring. - Otherwise (
extends Date ? string : T[P]), keep the original typeT[P].
UserAsJson would be:
interface UserAsJson {
id: number;
username: string;
email: string;
createdAt: string; // Transformed from Date
updatedAt: string; // Transformed from Date
}
The keyof operator is a powerful tool here because it works on any type that has named properties, including interfaces, type aliases, and even object literals.
A common pattern for advanced manipulation is to combine mapped types with lookup types and conditional types to modify properties based on their original type or name. For instance, imagine you want to create a type where all properties that are strings become optional, and all others remain required.
type OptionalStringsOnly<T> = {
[P in keyof T]: T[P] extends string ? T[P] | undefined : T[P];
};
This is a subtle but powerful technique: T[P] | undefined is equivalent to making the property optional when applied to a mapped type.
The as keyword in mapped types, introduced in TypeScript 4.1, allows you to remap the keys themselves, not just their types. This is useful for prefixing or suffixing properties, or for filtering properties based on their names.
type PrefixKeys<T, K extends string> = {
[P in keyof T as `${K}${string & P}`]: T[P];
};
type PrefixedUser = PrefixKeys<User, "user_">;
Here, as \${K}${string & P}`takes each keyPand transforms it into a new string literal by prependingK(which is"user_"in this case). Thestring & Ppart is a bit of type-level wizardry to ensureP` is treated as a string literal type, which is necessary for template literal types.
PrefixedUser would become:
interface PrefixedUser {
user_id: number;
user_username: string;
user_email: string;
user_createdAt: Date;
user_updatedAt: Date;
}
This ability to rename keys opens up a lot more possibilities for type transformations, like creating types with different naming conventions or filtering properties based on name patterns.
The next concept you’ll likely encounter is how to filter properties out of a mapped type, often using conditional types with never.