# TypeScript Thursday - S01E02 - Mapped types

## Introduction

The last episode [TypeScript Thursday - S01E01](https://nighttrax.dev/typescript-thursday-s01e01-generics) introduced generic types, how they work, and how we can use type inference for function calls. All the examples shown there took one or several generic type parameters and produced types that contained them *somewhere* e.g. `Box<T> = { content: T }`.

In this episode, we're going to look over some techniques for transforming the generic type parameters and producing more complex types. A very common mapped type that you might have already used is the built-in `Record` type that lets us define objects with arbitrary keys and known value types:

```typescript
const numberMap: Record<string, number> = {
  arbitrary: 1,
  keys: 2,
};
```

You may have also used `Pick` which lets us "pick" keys from an existing type:

```typescript
type Person = {
  firstName: string;
  lastName: string;
  age: number;
}

type Name = Pick<Person, 'firstName' | 'lastName'>;
//    ^? { firstName: string; lastName: string }
```

The article will cover those examples and much more advanced ones. But first, we have to go over some basics that we'll use later to build our mapped types. Read on!

## keyof

> `keyof` will come in handy later when we'll build mapped types that copy the keys from a different type.

The `keyof` type operator takes a type and returns a union of its keys:

```typescript
type Point = { x: number; y: number };

type Keys = keyof Point;
//    ^? 'x' | 'y'
```

It can also work in combination with generic types:

```typescript
type Keys<T> = keyof T;

type Foo = Keys<{ foo: string; bar: number };
//    ^? 'foo' | 'bar'
```

Using the operator on a union of types will give us only the keys that are common between the members.

```typescript
type Foo = { foo: string; bar: number }
type Bar = { bar: number; baz: () => void }

type Keys = keyof (Foo | Bar)
//    ^? 'bar'
```

This might seem counterintuitive, but consider the following:

```typescript
const get = (foo: Foo, bar: Bar, key: Keys) => {
  return foo[key] * bar[key];
}
```

If `keyof` would return the union of keys of both types, we might try to access a key that exists on `Bar`, but not on `Foo`.

What happens if we use the operator on a type that isn't an object?

```typescript
type K1 = keyof number;
//   ^? 'toString' | 'toFixed' | 'toExponential' ...
```

`keyof` on a `number` gives us all the properties available on numbers, such as `toString`, `toFixed`, `toExponential` etc.

```typescript
type K2 = keyof number[];
//   ^? 0, 1, 2, 3, 4, ..., push, pop,...

type K3 = keyof [number, boolean];
//   ^? 0, 1, push, pop, ...
```

`keyof` on any kind of an array will give us the numeric keys and the array-specific properties like `push` and `pop`. Using it on tuples will work similarly to arrays, but the numeric keys will be limited to the ones defined in the tuple type. Above we only have 2 members in the tuple, so the returned keys are `0` and `1`. For arrays we get an infinite number of positive numbers.

We can use `keyof` in combination with generic types to create a type-safe `sort` function that sorts an array of objects by a specific key:

```typescript
const sort = <T, K extends keyof T>(
  objects: T[],
  key: K
): T[] => {
  return objects.slice().sort((a, b) => {
    if (a[key] < b[key]) return -1;
    if (a[key] > b[key]) return 1;
    return 0;
  })
}

sort([{ foo: 10 }, { foo: 2 }], 'foo');

//               Argument of type '"bar"' is not assignable to
//               parameter of type '"foo"'.(2345)
//                                V
sort([{ foo: 10 }, { foo: 2 }], 'bar');
```

## Index signatures

> The syntax of index signatures is similar to that of mapped types, and their functionalities overlap quite a bit.

Index signatures allow us to model types that have an undefined number of structured key-value pairs. They're useful for representing configuration data, map/cache structures, arrays etc.

```typescript
type Visited = {
  [key: string]: boolean;
}
 
const visited: Visited = {
  node1: true,
  node2: false,
};
```

The square brackets `[]` syntax looks similar to computed properties on objects. The name of the key can be anything you'd like, and the type must be either `string`, `number`, or `symbol`. You can have at most one index signature of each type, and you can use unions to represent multiple signatures in one go.

```typescript
type MultipleSignatures = {
  // This expands to 2 index signatures.
  [key: string | number]: number;
}
```

When you have both a `string` and `number` index signature, the `number` signature must be a subtype of the `string` signature. This is because `foo[1]` is converted at runtime to `foo['1']`. TypeScript lets us distinguish between the two so we can model more accurate types.

```typescript
type MixedSignatures = {
  // 'number' index type 'string | number' is not assignable
  // to 'string' index type 'string'.(2413)
  [numberKey: number]: number | string;
  [stringKey: string]: string;
}
```

This restriction doesn't apply to `symbol` index signatures.

```typescript
type MixedSignatures = {
  [symbolKey: symbol]: boolean;
  [stringKey: string]: string;
}
```

Literal types and generic types are not allowed, even if they're of type `string | number | symbol`:

```typescript
type Incorrect = {
  // An index signature parameter type cannot be a literal type or
  // generic type. Consider using a mapped object type instead.(1337)
  [literal: 'a' | 'b']: string;
}
```

Since index signatures allow any number of keys of the given type, TypeScript will let us access arbitrary keys even if they don't exist at runtime:

```typescript
type Visited = { [city: string]: boolean; }
const visited: Visited = { boston: true };

console.log(visited.boston); // true
//                    ^? boolean

console.log(visited.london); // undefined
//                    ^? boolean
```

To guard against potential mistakes, you can turn on the compiler flag [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) which will add `undefined` to the value type for index signatures, so both properties above would return `boolean | undefined`. This can prove especially useful for arrays, to prevent out-of-bounds access:

```typescript
type NumberArray = {
  [K: number]: number;
}

const array: NumberArray = [1, 2, 3];

console.log(array[6]);
//                ^? `number | undefined` with the flag on
```

The flag makes index signatures behave similarly to `Map`:

```typescript
const map = new Map<string, number>();
map.set('foo', 42);

console.log(map.get('foo')); // 42
//                ^? `number | undefined`

console.log(map.get('bar')); // undefined
//                ^? `number | undefined`
```

We can combine index signatures with known properties as long as the types match:

```typescript
type KnownKeysAndSignature = {
  [index: string]: number;
 
  // OK, matches the string index signature.
  length: number;

  // Property 'name' of type 'string' is not assignable
  // to 'string' index type 'number'.(2411)
  name: string;
}
```

Using the `keyof` operator on an index signature gives us the key type:

```typescript
type StringMap = {
  [key: string]: string
}

type MixedMap = {
  [key: number | symbol]: string;
  known: string;
}

type K1 = keyof StringMap;
//   ^? string | number

type K2 = keyof MixedMap;
//   ^? number | symbol | 'known'
```

You'll notice that `keyof` on the string signature gave `string | number`, because of the aforementioned conversion of numeric keys to string keys. `keyof` will also return any known keys that are mixed with the index signatures.

## Indexed access types

> We'll use indexed access types later to build mapped types that copy the values from a different type.

Just like we can access a property of an object using the square bracket notation `foo['bar']`, we can also look up a specific property on a type:

```typescript
type Person = {
  name: string;
  age: number;
  employed: boolean;
};

type Age = Person["age"];
//    ^? number
```

We can use unions to get the type of more than one key:

```typescript
type Person = {
  name: string;
  age: number;
  employed: boolean;
};

type Keys = 'name' | 'age';

type Values = Person[Keys];
//     ^? string | number
```

We can also use the `keyof` operator to get the types of all the keys:

```typescript
type Person = {
  name: string;
  age: number;
  employed: boolean;
};

type Bar = Person[keyof Person];
//    ^? string | number | boolean
```

If we have a type with an index signature we can use the key type to get the value type:

```typescript
type Visited = {
  [key: string]: boolean;
};

type Value = Visited[string];
//     ^? boolean
```

Using an indexed access type on a union will give us a union, as long as all the union members have the type we're accessing:

```typescript
type Foo = { foo: string }
type Bar = { foo: number }
type Baz = { bar: boolean }

type K1 = (Foo | Bar)['foo']
//   ^? string | number

// Property 'foo' does not exist on type 'Foo | Baz'.(2339)
//                      V
type K2 = (Foo | Baz)['foo']
```

We can use indexed access types to create a type-safe `dispatch` method that changes its `payload` type according to the `action`:

```typescript
enum Action { A, B, C }

type Payloads = {
  [Action.A]: number;
  [Action.B]: { foo: string };
  [Action.C]: string;
}

const dispatch = <T extends Action>(
  action: T,
  payload: Payloads[T]
) => {}

dispatch(Action.A, 42);
dispatch(Action.B, { foo: 'bar' });
// Argument of type 'boolean' is not assignable
// to parameter of type 'string'.(2345)
//                   V
dispatch(Action.C, false);
```

We can also use them in combination with generic types to create a function that "plucks" a key from an array of objects and returns the proper value type:

```typescript
const pluck = <T, K extends keyof T>(
  objects: T[],
  key: K
): T[K][] => {
  return objects.map(o => o[key]);
}

const foos = pluck([{ foo: 1 }, { foo: 2 }], 'foo');
//     ^? number[]
```

## Intermission

The previous sections cover important building blocks for mapped types, so let's recap what we've learned so far before continuing:

* an index signature `{ [key: number]: number }` can have any number of keys, including 0
    
* you can have at most 1 index signature per key type (`number`, `string`, `symbol`)
    
* the `number` index signature must be a subtype of the `string` index signature if both are present
    
* you can combine an index signature with known keys, as long as they're compatible
    
* the `keyof` operator returns the type of keys for the given type, which will be a subtype of `number | string | symbol`
    
* we can pick a subset of keys from a type using an indexed access type `Person['age']`
    

```typescript
type Foo = {
  [key: number]: number;
  known: string;
}

type Keys = keyof Foo;
//    ^? number | 'known'

type Bar = Foo['known'];
//    ^? string
```

## Mapped types

Mapped types are types created by iterating (or *mapping*) over a list of keys. The list of keys can come from a union (like in the example below), a `keyof` type, or a generic type parameter.

```typescript
type Keys = 'a' | 'b';

// { a: number; b: number }
type NumberMap = {
  [K in Keys]: number
}
```

The syntax looks very similar to an index signature, but we have the `in` keyword between the name of the key and the type of the key. The type of the key must be `string | number | symbol`, just like index signatures, but we can also use literal types. We can still create an index signature with a mapped type, but it doesn't provide any extra value:

```typescript
type NumberMap = {
  // A regular index signature.
  [K in number]: number
}
```

We can also use generic types for the keys, given they're constrained to `string | number | symbol`:

```typescript
type NumberMap<Keys extends string> = {
  [K in Keys]: number;
}

const map: NumberMap<'a' | 'b'> = {
  a: 1,
  b: 2
};
```

Notice how our constraint for the keys is `string`, not `string[]`. You can think of `in` as iterating over a list, but the type has to be a **union**, not an array. If we use the `keyof` operator over the generic type parameter then that constraint is automatically taken care of, as the operator will always return `string | number | symbol`:

```typescript
type Person = {
  name: string;
  age: number;
}

type Numberify<T> = {
  [K in keyof T]: number
}

type Baz = Numberify<Person>;
//    ^? { name: number; age: number }
```

The example above iterates over the keys of one type and changes the value type to `number`. We can use an indexed access type to preserve the original value type:

```typescript
type FormValues<T> = {
  [K in keyof T]: {
    // Get the type in `T` for the value at key `K`.
    //      VV
    value: T[K];
    label: string;
  }
}

type Person = { name: string; age: number }
//                      ^             ^
//                      |             |
//                      |             +-------+
//                      |                     |
//                      +-----------------+   |
//                                        |   |
const options: FormValues<Person> = { //  |   |
//  +----------`string`-------------------+   |
//  |              |                          |
//  V              V                          |
  name: { value: 'Bob', label: 'Name' }, //   |
//  +--------`number`-------------------------+
//  |           |
//  V           V
  age: { value: 42, label: 'Age' },
}
```

Using all of the above we can re-create TypeScript's built-in `Record` and `Pick` types:

```typescript
type Record<K extends string | number | symbol, V> = {
  [Key in K]: V
}

const map: Record<'age' | 'height', number> = {
  age: 42,
  height: 180
};
```

```typescript
type Pick<T, K extends keyof T> = {
  [Key in K]: T[Key];
}

type Bar = Pick<Person, 'age'>;
//    ^? { age: number }
```

The `K extends keyof T` constraint means that the list of keys we pass in must be a subset of all the keys of `T`. This prevents us from trying to pick an incorrect key.

```typescript
// Type '"oops"' does not satisfy the constraint 'keyof Foo'.(2344)
//                        VV
type Bar = Pick<Person, 'oops'>;
```

## Mapping modifiers

When mapping keys we can change their mutability and/or their optionality with the `readonly` and `?` modifiers. We can remove the modifiers with `-`, or add them with `+`. `+` is assumed by default so we can leave it out.

```typescript
type Person = {
  name: string;
  readonly born: Date;
  died?: Date;
}
```

The above type contains a mix of immutable and optional properties, so let's see how we can change them using a mapped type.

```typescript
type Partial<T> = {
  [K in keyof T]?: T[K];
}

type MaybePerson = Partial<Person>;
//     ^? { name?: string; readonly born?: Date; died?: Date }
```

The first example showcases the built-in `Partial` type which makes all mapped properties optional.

```typescript
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
}

type ReadonlyPerson = Readonly<Person>;
//      ^?  { readonly name: string; readonly born: Date; readonly died?: Date }
```

Here we make all mapped properties immutable using the built-in `Readonly` type.

```typescript
type Required<T> = {
  [K in keyof T]-?: T[K];
};
 
type RequiredPerson = Required<Person>;
//      ^? { name: string; readonly born: Date; died: Date }
```

This example removes the optionality with the built-in `Required` type.

```typescript
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
}

type MutablePerson = Mutable<Person>;
//     ^? { name: string; born: Date; died?: Date }
```

The above example removes the immutability of all mapped properties and is the only type that's not built-in.

```typescript
type MutableAndRequired<T> = {
  -readonly [K in keyof T]-?: T[K];
}

type CleanedUpPerson = MutableAndRequired<Person>;
//     ^? { name: string; born: Date; died: Date }
```

Lastly, the above example shows how we can mix both modifiers at the same time.

## Homomorphic mapped types

Whenever a mapped type uses `keyof` to get the list of keys, or a generic type parameter with an `extends keyof` constraint, it becomes a [homomorphic](https://en.wikipedia.org/wiki/Homomorphism) mapped type. This gives it a few interesting properties, the first being that TypeScript will preserve the property modifiers from the input type:

```typescript
type Person = {
  name: string;
  readonly born: Date;
  died?: Date;
}

//                   modifiers are copied over
//                    |                     |
//                    V                     V
// { name: string; readonly born: Date; died?: Date }
type HomomorphicMapping = {
  [K in keyof Person]: Person[K];
}
```

Compare the above with the non-homomorphic mapped type below which hardcodes the list of keys:

```typescript
//            modifiers are not copied over
//                   |           |
//                   V           V
// { name: string; born: Date; died: Date }
type NonHomomorphicMapping = {
  [K in 'name' | 'born' | 'died']: Person[K];
}
```

Even though we're still iterating over the keys from the `Person` type, and we're using the same indexed access type for the values, TypeScript no longer sees the relationship between the input type `Person` and our mapping. Non-homomorphic types are essentially creating new properties, so they can’t copy property modifiers from anywhere.

The other property homomorphic mapped types have is they preserve tuples and arrays instead of converting them to object types:

```typescript
type Coordinate = [number, number];

type HomomorphicPromisify<T> = {
  [K in keyof T]: Promise<T[K]>;
};

type PromiseCoordinate = HomomorphicPromisify<Coordinate>;
//     ^? [Promise<number>, Promise<number>]

type Length = PromiseCoordinate['length'];
//     ^? 2
```

Notice how the `length` property was kept as `number`, and not mapped to `Promise<number>` by our mapped type. Even though `keyof` on arrays and tuples return the numeric keys and methods and properties such as `length`, homomorphic types will not apply any transformations to them.

> TypeScript has a bug ([microsoft/TypeScript#27995](https://github.com/microsoft/TypeScript/issues/27995#issuecomment-433056847)) that treats mapping over non-generic tuples as non-homomorphic.
> 
> Moreover, another bug [microsoft/TypeScript#/27351](https://github.com/microsoft/TypeScript/issues/27351) may prevent using an indexed access type over constrained arrays.

Lastly, homomorphic mapped types can be "unwrapped" to get the original type, even if the new type only copied a subset of keys, such as in the case of `Pick`:

```typescript
const unwrap = <T, K extends keyof T>(partial: Pick<T, K>): T => {}

const partial: Pick<Person, 'name'> = {
  name: 'Bob'
}

const original = unwrap(partial);
//       ^? Person
```

In general, most mapped types you would create, or come across, would be homomorphic. The built-in `Partial`, `Pick`, `Readonly` and `Required` types are homomorphic, while `Record` is not, because it receives the set of keys directly, rather than copying them from another type.

## Key remapping

All of the previous examples keep the original keys from the type we're iterating on, but we can also change them using the `as` operator. Together with indexed access types, we can "re-index" a nested object by a different key:

```typescript
type Analytics = Record<string, { title: string; views: number }>;

type ViewsByTitle<T extends Analytics> = {
  [K in keyof T as T[K]['title']]: T[K]['views']
}

// { 'Home page': 42 }
type Bar = ViewsByTitle<{
  index: {
    title: 'Home page',
    views: 42
  }
}>
```

We can also use template literal types to rename the keys (stay tuned for a future episode that will cover them in more detail):

```typescript
type Prefix<T> = {
  [K in keyof T as `base_${K & string}`]: T[K];
}

type Bar = Prefix<{ foo: string }>
//    ^? { base_foo: string }
```

## Summary

Thank you for reading and I hope you enjoyed the article. Below you can find the key takeaways and a shareable sketch note. Make sure to catch the next episode  [TypeScript Thursday - S01E02](https://nighttrax.dev/typescript-thursday-s01e03-conditional-types), which will explore ways of filtering out certain keys in mapped types!

> If you liked this article, maybe you'd also like some of my open-source projects at [github.com/NiGhTTraX](https://github.com/NiGhTTraX).

Takeaways:

* index signatures create types with any number of keys
    
* they look like objects and use the square bracket `[]` syntax to denote the key type
    
* the `keyof` operator returns the union of keys for the given type
    
* an index signature `{ [key: number]: number }` can have any number of keys, including 0
    
* we can pick a subset of keys from a type using an indexed access type
    
* a mapped type constructs a type from a union of keys all having the same value type
    
* the syntax is similar to that of index signatures, but it adds the `in` keyword
    
* index signatures always have "infinite" keys, mapped types *can* have "finite" keys
    
* you can use generic types and/or `keyof` to get the keys from another type
    
* a mapped type is homomorphic if it copies all or a subset of keys from another type with `keyof`
    
* homomorphic mapped types preserve property modifiers, preserve tuples and arrays, and can be unwrapped to the original type
    
* you can add or remove `readonly` and `?` property modifiers with `+` and `-`
    
* TypeScript ships with `Record`, `Pick`, `Partial`, `Readonly` and `Required` types
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1676925695702/cf28f832-7513-4de6-aeff-802dcc69ab7a.png align="center")
