Mapped Types page

Learn how to create new types with mapped types!

Overview

Due to syntax similarity, let’s do a quick recap on using index signatures to define object types. Then we will dive into what mapped types are and how to define them, followed by practical applciations of mapped types. Finally, we will work through exercises building out some commonly used mapped types.

Index Signatures

Index signatures are a way to define object types when you know the general shape of any object but nothing more specific than the key type and the value type. Let’s imagine we have an object that looks something like this:

const bag = {
  maxPotion: 2,
  maxRevive: 23,
  luckEgg: 5,
  thunderStone: 1,
  /// etc.
};

The general shape of this object is a key of type string and a value of type number. We could define a type with an index signature to match this general definition.

type Bag = {
  [itemName: string]: number;
};

An index signature consists of two parts:

  1. The indexer – [itemName: string]
  • Defines the type the keys are allowed to be
  1. The property’s value type – number
  • Defines the type of the property

One thing to be aware of with defining types using index signature is the value may or may not be there, but typescript makes it seem as though it’s always present.

const bag: Bag = {
  maxPotion: 2,
};

bag.maxPotion; // TS says it’s a number
bag.maxRevive; // TS says it’s a number... but it’s not there...

With interfaces and other type definitions, you can add a ? to delineate the property is optional. This is not the case with types defined using index signatures. To achieve the optional type-safety with index signature type you can create a union with the value type and undefined.

type BagWithUndefinedUnion = {
  [itemName: string]: number | undefined;
};

const bag: BagWithUndefinedUnion = {
  maxPotion: 2,
};

bag.maxPotion; // TS says it’s a number | undefined
bag.maxRevive; // TS says it’s a number | undefined

Requiring Certain Properties

Types defined using index signatures are great since they provide a lot of flexibility in what can be associated with the type but what if we wanted to require something to be there? In these situations, you can add those properties after the general key definition.

type BagWithRequiredValue = {
  [itemName: string]: number;
  pokeBall: number;
};

// ERROR: Property 'pokeBall' is missing in type '{ maxPotion: number; }' but required in type 'BagWithRequiredValue'.ts(2741)
const bagWithoutPokeball: BagWithRequiredValue = {
  maxPotion: 2,
};

// All good!
const bagWithPokeball: BagWithRequiredValue = {
  pokeBall: 1,
};

When defining required properties, the required properties cannot violate the index signature types. In the example above pokeball: number conforms to [itemName: string]: number; however, if we change pokeball to be of type string TypeScript doesn’t allow it, since it’s a string indexing another string.

type BagWithRequiredValue = {
  [itemName: string]: number;

  // ERROR: Property 'pokeBall' of type 'string' is not assignable to 'string' index type 'number'.
  pokeBall: string;
};

The error above can be remedied by expanding the type for the value with a union.

type BagWithRequiredValue = {
  [itemName: string]: number | string;
  pokeBall: string; // All good!
};

Multiple Indexers

It is possible to have more than one indexer and have different value types for indices. To understand these mechanics, we first have to talk about what can and can’t be the type inside an indexer. TypeScript does not allow us to index with anything; it only lets us index with three types string, number, and symbol.

Tip: For objects to be keyed with a union or something besides these three try the Record utlity type!

These three types have a strange relationship

  • number is a proper subset of string
  • symbol is a proper subset of string
  • number and symbol are mutually exclusive

In order for TypeScript to provide type safety, it has to be able to differentiate between the keys passed in. If the indexer’s values have the same types, it doesn’t matter.

type IndexerDifferentSameValue = {
  [itemString: string]: number;
  [itemNumber: number]: number;
};

In order to have more than one indexer with different types, the indexer’s types must be mutually exclusive (no overlap). This means the only way we can have more than one indexer with different value types is for one indexer to be of type number and the other to be of type symbol.

type NonMutuallyExclusiveIndexers = {
  [itemString: string]: number;
  [itemNumber: number]: string; // ERROR: 'number' index type 'string' is not assignable to 'string' index type 'number'.
};

type MutuallyExclusive = {
  [keyNumber: number]: number;
  [keySymbol: symbol]: string;
};

The idea of mutual exclusion extends to individual properties, in the type above we have a shape for all number and all symbol and if we tried to throw in all string we’d get errors. But, if we only define certain strings (like we did with required properties), TypeScript would be able to make the delineation and provide type safety.

type AllThree = {
  [keyNumber: number]: number;
  [keySymbol: symbol]: string;
  additionalProperty: boolean;
  empty: {};
};

JavaScript Makes an Appearance

TypeScript is a superset of JavaScript and has to conform to its rules. This can cause some unexpected behavior when indexing with a number. An example of this is arrays. In JavaScript arrays are just objects indexed with numbers; however, in JavaScript, all object keys are coerced to strings.

const firstThreePokemon = ["Bulbasaur", "Ivysaur", "Venusaur"];

// Both are allowed
const bulbasaurWithNumber = firstThreePokemon[0];
const bulbasaurWithString = firstThreePokemon["0"];

This quirk extends to index signature types in TypeScript.

type PokemonNameList = {
  [index: number]: string;
};

// All three are valid and work the same
const firstThreePokemon: PokemonNameList = ["Bulbasaur", "Ivysaur", "Venusaur"];

const firstThreePokemonObjectNumberKeys: PokemonNameList = {
  0: "Bulbasaur",
  1: "Ivysaur",
  2: "Venusaur",
};

const firstThreePokemonObjectStringKeys: PokemonNameList = {
  "0": "Bulbasaur",
  "1": "Ivysaur",
  "2": "Venusaur",
};

// Both are allowed
const bulbasaurWithNumber = firstThreePokemon[0];
const bulbasaurWithString = firstThreePokemon["0"];

Readonly Property Modifier

While the optional syntax (?) isn’t supported on index signature types, the index signature syntax does allow for the readonly modifier. The readonly modifier marks a property as immutable on an object meaning it cannot be re-assigned once set. If we wanted to make our PokemonNameList type above unchangeable we could do it by putting the readonly modifier at the beginning of the declaration.

type ReadonlyPokemonNameList = {
  readonly [index: number]: string;
};

const firstThreePokemon: ReadonlyPokemonNameList = [
  "Bulbasaur",
  "Ivysaur",
  "Venusaur",
];

// ERROR: Index signature in type 'ReadonlyPokemonNameList' only permits reading.
firstThreePokemon[0] = "Pikachu";

This extends to any required property added to the type as well.

Note: required properties have access to all the syntaxes you are familiar with when defining object types with type and interface.

Sticking with our PokemonNameList example type, although it looks like an array and even uses the array syntax, it doesn’t have some of the more fundamental properties of an array, like .length.

// ERROR: Property 'length' does not exist on type 'ReadonlyPokemonNameList'
firstThreePokemon.length;

Often times in development we need to know the length of a list, but it is not something we want to allow developers to overwrite. To accomplish this we can tweak our definition to include a readonly required length property.

type ReadonlyPokemonNameList = {
  readonly [index: number]: string;
  readonly length: number;
};

const firstThreePokemon: ReadonlyPokemonNameList = [
  "Bulbasaur",
  "Ivysaur",
  "Venusaur",
];

const firstThreePokemonObject: ReadonlyPokemonNameList = {
  0: "Bulbasaur",
  1: "Ivysaur",
  2: "Venusaur",
  length: 3,
};

console.log(firstThreePokemon.length); // 3

Note: In JavaScript the firstThreePokemon variable does have a .length property since it is an Array. TypeScript however, is not aware that it is an array, instead it thinks it is a ReadOnlyPokemonList which is why generally speaking you should avoid defining your arrays using an index signature. Instead you should use Array<T> or the shorthand [].

Mapped Types

Mapped types are another way to generate types in TypeScript. Mapped types are a way to iterate through each key of an object type to create new types for those keys. Mapped types are generic types that extend upon the index signature syntax. The best way to understand it is to see it in action. Let’s take a look at a utility type that uses mapped types – Partial<T>.

type Pokemon = {
  name: string;
  moves: string[];
};

/**
 * type PartialPokemon {
 *  name?: string;
 *  moves?: string[]
 * }
 */
type ParitalPokemon = Parital<Pokemon>;

Partial<T> is a common utility type that maps over a type and makes all the properties in the type optional. Below is the definition of Partial<T>.

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

If the phrase P in keyof T seems to bear some resemblance to a JavaScript for-in loop, that’s good! We’re essentially doing the same thing but with types. What Partial does is iterate through each of the properties, make them optional, and assign them whatever types they had before. Getting rid of some of the additional TypeScript in the mapping and looking at a concrete example helps illustrate this.

If we look at PartialPokemon and get rid of the generics we could re-write this as such.

type PartialPokemon = {
  [P in keyof Pokemon]?: Pokemon[P];
};

Since we know what keyof Pokemon evaluates too, let’s substitute that out.

type PartialPokemon = {
  [P in "name" | "moves"]?: Pokemon[P];
};

P serves as a variable for mapping and can be named anything, let’s name it something more semantically relevant.

type PartialPokemon = {
  [KeyName in "name" | "moves"]?: Pokemon[KeyName];
};

If we iterate through the key names we get something that looks like this.

type ParitalPokemon = {
  name?: Pokemon["name"];
  moves?: Pokemon["moves"];
};

Evaluating the index accessed types then leaves us with our final type.

type ParitalPokemon = {
  name?: string;
  moves?: string[];
};

Property Modifiers

We saw it a little bit when looking at Partial but mapped types give us the opportunity to change two things about the properties of the type we are creating – whether or not it’s optional and whether or not it’s immutable. Like types defined with an index signature, the properties of a mapped type can be made immutable by applying the readonly modifier. Using the readonly modifier in a mapped type is exactly how the Readonly utility type in TypeScript works.

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Additionally, we can remove a readonly modifier in a mapped type. To accomplish this we need a small tweak in our mapped syntax. Instead of readonly, we add -readonly, essentially subtracting off the readonly modifier.

type Changeable<T> = {
  -readonly [P in keyof T]: T[P];
};

In Partial we saw how we can create an optional property by adding the ?. We can remove optionality the same way we remove the readonly modifier – with a -?. This is best illustrated by the Required utility type.

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

Note: you can also add +readonly and +? to your types. They are default though so they are often omitted for brevity.

Remapped Keys

TypeScript allows us to map over more than just the keys of the object. We can do this using a syntax very similar to type assertion (as). To illustrate this look at the following types.

type PokeballItem = {
  item: "pokeballs";
  type: "great" | "normal" | "ultra" | "master";
  amount: number;
};

type BerryItem = {
  item: "berries";
  type: "oran" | "sitrus" | "pecha";
  amount: number;
};

type TMItem = {
  item: "tms";
  name: string;
  amount: number;
};

type Items = PokeballItem | BerryItem | TMItem;

We’d like to make a bag type with properties on it matching the name of the item property on each of the Items and having that be a function to return the amount (so bag.berries is a function with this shape () => number). To do this we must do two things -- constrain the generic and remap via as.

type MakeBag<T extends { item: string; amount: number }> = {
  [Item in keyof T as T["item"]]: () => T["amount"];
};

/**
 * {
 *  berries: () => number;
 *  tms: () => number;
 *  pokeballs: () => number;
 * }
 */
type Bag = MakeBag<Items>;

Let’s Make our own!

So far we’ve seen some mapped types that TypeScript provides for us in its utility types, so let’s walk through making our own. In this case, let’s look at creating a Pokemon class whose constructor takes the types of the Pokemon. So far in the examples leading up to this, the type property of a Pokemon has been defined as a union "Normal" | "Fire" | ..., but Pokemon can have more than one type, in fact, they can have up to two, like, in the case of Charizard – Fire and Flying. To create our class we want a single pokemon type (either a PokemonType or a [PokemonType]) or tuple that provides these options.

const pikachu = new Pokemon("Electric");
const charizard = new Pokemon(["Fire", "Flying"]);
const gyarados = new Pokemon(["Flying", "Water"]);

We may at first think something like this might work.

type PokemonType =
  | "Normal"
  | "Fire"
  | "Water"
  | "Grass"
  | "Electric"
  | "Ice"
  | "Fighting"
  | "Poison"
  | "Ground"
  | "Flying"
  | "Psychic"
  | "Bug"
  | "Rock"
  | "Ghost"
  | "Dark"
  | "Dragon"
  | "Steel"
  | "Fairy";

type PokemonTypes = [PokemonType] | [PokemonType, PokemonType];

class Pokemon<T extends PokemonType> {
  constructor(public types: T | PokemonTypes[T]) {}
}

However, this has a flaw: it allows us to have duplicate types such as ["Fire", "Fire"]. In this case, we need a type that has the original pokemon type and the list of the rest of the pokemon types. To create this we can use a mapped type that uses Exclude within its iteration.

type PokemonTypes = {
  [T in PokemonType]: [T] | [T, Exclude<PokemonType, T>];
};

What this gives us is a type that has either a single pokemon type or a tuple with a single pokemon type and the rest of the pokemon types with the original pokemon type excluded.

type PokemonTypes = {
  Normal: ["Normal"] | ["Normal", "Fire" | "Water" | "Grass" | "Electric" | "Ice"  | "Fighting" | "Poison" | "Ground" | "Flying" | "Psychic" | "Bug" | "Rock" | "Ghost" | "Dark" | "Dragon" | "Steel" | "Fairy"]
  Fire: ["Fire"] | ["Fire", "Normal" | "Water" | "Grass" | "Electric" | "Ice"  | "Fighting" | "Poison" | "Ground" | "Flying" | "Psychic" | "Bug" | "Rock" | "Ghost" | "Dark" | "Dragon" | "Steel" | "Fairy"]]
  /// rest of pokemon types
}

Now we can use them in our class. Like we said above, we want to be able to pass any of the following.

const pikachu = new Pokemon("Electric");
const charizard = new Pokemon(["Fire", "Flying"]);
const gyarados = new Pokemon(["Flying", "Water"]);

To do this we can use a generic constrained to our PokemonType and an index accessed type to create our class definition.

class Pokemon<T extends PokemonType> {
  constructor(public types: T | PokemonTypes[T]) {}
}

const pikachu = new Pokemon("Electric");
const charizard = new Pokemon(["Fire", "Flying"]);
const gyarados = new Pokemon(["Flying", "Water"]);

As we’ve seen mapped types can do much of the heavy lifting when it comes to creating types from types. They power many utility types leveraged in applications. Moving forward, we will only see them more frequently, especially as we move into our next section – conditional types.

Exercises

Exercise 1

Below is a generic type called To<T,K> that is currently set to any. Update the type to change all of the properties on T to whatever is passed into K. Take the following ToNumber type for example, it serves as an alias for To where K is number.

type ToNumber<T> = To<T, number>;
type Numberfied = ToNumber<{ level: string; age: string }>; // {level: number; age: number}

Open in CodeSandbox

/**
 * Exercise 1
 * Below is a generic type called `To<T,K>` that is currently set to `any`. Update the type to change all of the properties
 * on `T` **to** whatever is passed into `K`. Take the following `ToNumber` type for example, it serves as an alias for `To` where
 * `K` is `number`.
 *
 * ```ts
 * type ToNumber<T> = To<T, number>;
 * type Numberfied = ToNumber<{level: string; age: string;}> // {level: number; age: number}
 * ```
 */
type To<T, K> = any; // TODO: don’t use any

const initialState = {
  name: "",
  emailAddress: "",
  age: 0,
};

type State = typeof initialState;
type StateValidation = To<State, boolean>;

const stateValidationSafe: StateValidation = {
  name: false,
  emailAddress: true,
  age: false,
};

// This should fail if the type is correct
const FAILURE_stateValidationTypeError: StateValidation = {
  name: 99, // Type 'number' is not assignable to type 'boolean'.ts(2322)
  emailAddress: "bob", // Type 'string' is not assignable to type 'boolean'.ts(2322)
  age: NaN, // Type 'number' is not assignable to type 'boolean'.ts(2322)
};

Click to see the solution

export type To<T, K> = {
  [Key in keyof T]: K;
};

Exercise 2

Exercise 2:

Let’s recreate the Pick utility type. _Pick should take two generics, some object T and a string literal union that is some subset of keys from T as K.

type _Pick<T, K> = any; // TODO

type Picked = _Pick<{ name: string; age: number }, "age">; // {age: number}

Hint for Exercise 2 (click to reveal) You may need to update the definition of K to get this type to work properly

Open in CodeSandbox

/**
 * Exercise 2:
 *
 * Let’s recreate the `Pick` utility type. `_Pick` should take two generics, some object `T` and a string literal union that is
 * some subset of keys from `T` as `K`.
 *
 * ```ts
 * type _Pick<T, K> = any; // TODO
 *
 * type Picked = _Pick<{ name: string; age: number }, "age">; // {age: number}
 * ```
 *
 * > Hint:
 * > You may need to update the definition of `K` to get this type to work properly
 */
type _Pick<T, K> = any;

Click to see the solution

/**
 * From T, pick a set of properties whose keys are in the union K
 */
export type _Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};