← Concepts

Mapped Types

Mapped types are a feature in TypeScript which allow you to map over a union of types to create a new type.

The syntax looks like this:

type Fruit = "apple" | "banana" | "orange";

type NewType = {
  [F in Fruit]: {
    name: F;
  };
};
/**
 * {
 *   apple: { name: "apple" };
 *   banana: { name: "banana" };
 *   orange: { name: "orange" };
 * }
 */

Let's break down each piece of syntax we're seeing here. The F in Fruit acts as a kind of index signature, which allows us to loop over each member of the Fruit union. You can think of this as being similar to a JavaScript for...of loop:

const fruit = ["apple", "banana", "orange"];

for (const f of fruit) {
  console.log(f);
}

In both cases, we get a very important thing: a closure over the current thing we're iterating over. In the JavaScript example, we get the current item. In the TypeScript example, we get the current member of the union.

This ability to cleanly map over each member of a union in a simple for...of model is what makes mapped types so powerful.

With keyof

Using the keyof operator with mapped types gives you a smooth API to create object types from other object types.

interface Person {
  name: string;
  age: number;
}

type NullablePerson = {
  [P in keyof Person]: Person[P] | null;
};

/**
 * {
 *   name: string | null;
 *   age: number | null;
 * }
 */

Here, we gain access to P, which represents either name the first time this is iterated over, then age the second time. This means we can gain access to the type of each property's value in Person by using Person[P].

We can then use this to create a new type that has the same keys and values as Person but with each property being nullable.

Mapping non-string unions with as

Sometimes, you'll have a union of things which can't be assigned to the key of an object. For instance, a union of objects:

type Event =
  | {
      type: "click";
      x: number;
      y: number;
    }
  | {
      type: "hover";
      element: HTMLElement;
    };

Here, you can see that Event is a union of two objects. If we tried to do [E in Event], we'd get an error:

type EventMap = {
  // Type 'Event' is not assignable to type 'string | number | symbol'.
  [E in Event]: (event: E) => void;
};

To get around this, we can use the as keyword to tell TypeScript that we want to use the type property of each E as we iterate over it:

type EventMap = {
  [E in Event as E["type"]]: (event: E) => void;
};

/**
 * {
 *   click: (event: { type: "click"; x: number; y: number; }) => void;
 *   hover: (event: { type: "hover"; element: HTMLElement; }) => void;
 * }
 */