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

  • K is a union of property keys.
  • P iterates over each key in K.
  • T is the new type for the property P.

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:

  1. keyof T: This extracts a union of all property names from the type T. For User, this would be "id" | "username" | "email" | "createdAt" | "updatedAt".
  2. [P in keyof T]: This is the mapped type syntax. It says, "for each property name P in the union keyof T…"
  3. ?: T[P]: "…create a new property with the name P, make it optional (the ?), and give it the type T[P]." T[P] is a lookup type, meaning "the type of the property P in type T."

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]) is Date, then the new type is string.
  • Otherwise (extends Date ? string : T[P]), keep the original type T[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.

Want structured learning?

Take the full Typescript course →