Notes on TypeScript: Mapped Types and Lookup Types
Tags: typescript, types
Published: 2019-02-24 19:11:49

---
title: Notes on TypeScript: Mapped Types and Lookup Types
published: true
description: Better understanding Mapped Types and Lookup Types
tags: typescript, types
series: Notes on TypeScript
---

### Introduction

These notes should help in better understanding `TypeScript` and might be helpful when needing to lookup up how leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

### MappedTypes

In this part of the "Notes on TypeScript" series we want to level up on our type level programming knowledge in TypeScript. To get a better fundamental understanding of the topic, we will revisit some topics we discussed in ["Type Level Programming Part 1"](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-in-typescript-part-1-i57) and see in more depth how these types are implemented. To be more specific let's gain a better understanding of __Mapped Types__.

In a [previous post](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-in-typescript-part-1-i57) we implemented a Partial, a Required and a ReadOnly type.

```js
type User = {
  id: number;
  name: string;
};

type MakeReadOnly<Type> = {readonly [key in keyof Type ]: Type[key]};
// Test MakeReadOnly
type ReadOnlyUser =  MakeReadOnly<User>;

/*
type ReadOnlyUser = {
  readonly id: number;
  readonly name: string;
}
*/
```

We implemented these type without discussing in more depth, the actual underlying mechanism, which we will do now. Taking a look at our `MakeReadOnly`, we notice that we can map over the property types enabling to create a new type. In the specific example above, we mapped over all the property types and transformed them to _readonly_.

Let's take a look at another example we previously implemented.

```js
type MakePick<Type, Keys extends keyof Type> = { [Key in Keys]: Type[Key] };
```

Our `MakePick`, which reflects the provided `Pick`, goes through all the keys, that extend the provided `Type` and returns a new type.

```js
type User = {
  id: number;
  name: string;
  points: number;
};
type TestMakePick = MakePick<User, "id" | "name">;

/*
type TestMakePick = {
  id: number;
  name: string;
};
*/
```

This is what is actually happening when we try to select the id and name from `User`:

```js
type TestMakePick = { [Key in "id" | "name"]: User[Key] };

/*
type TestMakePick = {
  id: number;
  name: string;
};
*/
```

Using lookup types, which we will explore in more depth in the next section, we can rewrite the above example to the following:

```js
type TestMakePick = {
  id: User["id"],
  name: User["name"]
};

/*
type TestMakePick = {
  id: number;
  name: string;
};
*/
```

The above transformations, should help us get a better idea of how mapped types work.

### Lookup Types

To build more advanced types, let's look at two interesting TypeScript features __Lookup types__ and __keyof__.

```js
type UserKeyTypes = User["id" | "name" | "points"];

/*
type UserKeyTypes = number | string;
*/
```


By using `keyof`, we can rewrite the above type to the following:

```js
type UserKeyTypes = User[keyof User];

/*
type UserKeyTypes = number | string;
*/
```

`keyof` can help us avoid having to manually define all the keys, rather letting TypeScript provide the keys, which helps in avoiding having to keep updating these types once they change.

```js
type UserKeys = keyof User;

/*
type UserKeys = "id" | "name" | "string";
*/
```

Lookup types, also referred to as indexed access types enable us to access the types for provided keys. Similar to how we can access the values of object properties, but for types.

```js
type UserNameType = User["name"];

/*
type UserNameType = string;
*/
```

We can also look up a number of keys as seen in the first example we wrote in this section:

```js
type UserKeyTypes = User["id" | "name" | "points"];

/*
type UserKeyTypes = number | string;
*/
```

This is an interesting feature and enables developers to provide more explicit type handling when dealing with object properties. Let's replicate a simpler (sans currying) implementation of the `prop` and `assoc` functions from the __Ramda__ library.

```js
function prop(obj, key) {
  return obj[key]
}
```

One way to type the `prop` function would be to provide an object type and define the key as a string. But TypeScript will not be able to infer the return type.

We need to be more explicit about the key type, which we can achieve by guaranteeing that the key type extends the provided object key types via defining: `Key extends keyof Type`.

```js
function prop<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "Test User",
  points: 0
};

const userName = prop(user, "name"); // const userName : string;
```

The interesting aspect here, is that TypeScript can now infer the return type of the return value:

```js
return obj[key]; // => Type[Key]
```

Another benefit we get by leveraging lookup types and keyof is that we can ensure that only existing property keys can be passed to `prop`.

```js
// The following will result in TypeScript complaining:
const userName = prop(user, "status");
// Argument of type '"status"' is not assignable to parameter of type '"id" | "name" | "points"'
```

So trying to access an non existent property will cause TypeScript to complain that the provided type is not assignable. To gain more understanding and validate what we have learned so far, let's implement `assoc`.

```js
function assoc<Type, Key extends keyof Type>(
  obj: Type,
  key: Key,
  value: Type[Key]
) {
  return { ...obj, [key]: value };
}
```

`assoc` looks similar to the `prop` function, but as we are also providing a value this time, we can use lookup types to define the expected value type via `Type[Key]`.

```js
const updatedUserName = assoc(user, "name", "User Test A");
const updatedUserPoints = assoc(user, "points", 0);

// The following will examples result in TypeScript complaining:

const updatedUserPoint = assoc(user, "point", 0);
// Argument of type '"point"' is not assignable to parameter of type '"id" | "name" | "points"'

const updatedUserPointsAsString = assoc(user, "point", "0");
// Argument of type '"0"' is not assignable to parameter of type 'number'
```

Via leveraging lookup types we can guarantee that the expected update value has the correct type.

### Mapped Types and Lookup Types

Finally, let's revisit a more advanced example we built when working with [conditional types](https://dev.to/busypeoples/notes-on-typescript-conditional-types-4bh).

```js
type RemoveUndefinable<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
}[keyof Type];

type RemoveNullableProperties<Type> = {
  [Key in RemoveUndefinable<Type>]: Type[Key]
};

type TestRemoveNullableProperties = RemoveNullableProperties<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveNullableProperties = {
  id: number;
  name: string;
};
*/
```

The `RemoveNullableProperties` conditional type expects a type and returns a new type only containing non nullable property types. It's a good idea to break the existing implementation into multiple parts as we have a better understanding of mapped and lookup types now.

```js
type RemoveUndefinable<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
}[keyof Type];
```

Let's start with `RemoveUndefinable` first:

```js
type RemoveUndefinableKeys<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
};

type TestRemoveUndefinableKeys = RemoveUndefinable<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveUndefinableKeys = {
  id: "id";
  name: "name";
  property?: undefined;
}
*/
```

We get a new type containing the key names or undefined depending wether the type extends undefined or not. If you recall, we can lookup multiple type like so: `{a: number; b: string; c: number[]}["a" | "b"]`, which means we can use a lookup to extract all the relevant keys for the provided types in the next step.

```js
type RemoveUndefinableKeys<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
};

type RemoveUndefinable<Type> = RemoveUndefinableKeys<Type>[keyof Type];

type TestRemoveUndefinable = RemoveUndefinable<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveUndefinable = "id" | "name" | undefined;
*/
```

`RemoveUndefinable` returns all the mapped key names, so our next step is remove any non existent keys from the provided type.

```js
type RemoveUndefinableKeys<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
};

type RemoveUndefinable<Type> = RemoveUndefinableKeys<Type>[keyof Type]>;

type RemoveNullableProperties<Type> = {
  [Key in RemoveUndefinable<Type>]: Type[Key]
};

type TestRemoveNullableProperties = RemoveNullableProperties<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveNullableProperties = {
  id: number;
  name: string;
};
*/
```

We can exchange the `RemoveNullableProperties` with the TypeScript provided `Pick` type, as the implementation of `Pick` is similar to the `RemoveNullableProperties` implementation:

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

Exchanging `RemoveNullableProperties` with `Pick` leaves us with the following implementation:

```js
type RemoveUndefinableKeys<Type> = {
  [Key in keyof Type]: undefined extends Type[Key] ? never : Key
};

type RemoveNullableProperties<Type> = Pick<
  Type,
  RemoveUndefinableKeys<Type>[keyof Type]
>;

type TestRemoveNullableProperties = RemoveNullableProperties<{
  id: number;
  name: string;
  property?: string;
}>;

/*
type TestRemoveNullableProperties = {
  id: number;
  name: string;
};
*/
```

We should have a good understanding of mapped types, lookup types and keyof and how to leverage them when working with TypeScript. The knowledge gained in this write-up should help us to build more advanced type level programming examples in the upcoming "Notes on TypeScript", which will be focusing on more advanced type level programming.

### Links
[TypeScript 2.1 Release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types)

[TypeScript 2.1: Mapped Types](https://mariusschulz.com/blog/typescript-2-1-mapped-types)

[TypeScript 2.1: keyof and Lookup Types](https://mariusschulz.com/blog/typescript-2-1-keyof-and-lookup-types)

[Notes on TypeScript: Type Level Programming Part 1](https://dev.to/busypeoples/notes-on-typescript-type-level-programming-in-typescript-part-1-i57)

[Notes on TypeScript: Conditional Types](https://dev.to/busypeoples/notes-on-typescript-conditional-types-4bh)

If you have any questions or feedback please leave a comment here or connect via Twitter: [A. Sharif](https://twitter.com/sharifsbeat)