Mastering Mapped Types in TypeScript and it's Practical Applications
Tags: typescript, tutorial, javascript, webdev
Published: 2024-06-04 07:28:52

## Introduction

When I was new to typescript, I used to write basic interfaces and object types. I didn’t you use to pay much attention towards the reason of these types and nor use to think it from scabality perspective. 

I later on decided to level up myself in the TS game. So I started with the [typescript challenges](https://github.com/type-challenges/type-challenges?tab=readme-ov-file) where I began with easy problems then went on to the complex ones. The challenges are well structured so that you can learn the basics. But what helped me the most was to analyze the solution of other folks on [Github Issues](https://github.com/type-challenges/type-challenges/issues) and [Michgan Typescript’s youtube channel](https://www.youtube.com/playlist?list=PLOlZuxYbPik180vcJfsAM6xHYLVxrEgHC). This increased my understand of Typescript by 10x. 

These challenges has inspired me to write code that is:

- Scalable
- Structured
- Typesafe
- Less error prone

I started to understand Typescript in a deeper way. All these challenges has inspired me to write this blog post about mapped types in Typescript. So in this blogpost, I am going to talk about Mapped typescript type in brief and explains its real-life applications in detail.

<aside>
💡 **NOTE:** These application of mapped types can be found in the challenges of typescript shared above.

</aside>

## What are Mapped Types in Typescript?

Mapped types in TS is nothing but a way through which you can traverse the properties of the typescript object type and modify them as you require.

To quote the [TS cheatsheet](https://www.typescriptlang.org/cheatsheets/):

*Mapped Types acts like a map statement for the type system, allowing an input to change the structure of the new type*

Consider the following the following type:

```tsx
type Vehicle = {
	type: 'Car',
	noOfWheels: 4,
	color: 'white'
}
```

Now imagine that you need to generate a new type from the `Vehicle` type such that all the parameters are optional. Here mapped types can be used to effectively get this requirement right like below:

```tsx
type Car = {
	[P in keyof Vehicle]?: Vehicle[P] 
}
```

This would generate all the properties that are optional. This is a simple use case but mapped types can do a lot more exciting stuffs. You can read how you can modify the properties in the mapped typed in this section of the TS docs: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers

With this understanding of mapped types, let us start with some real life uses cases

## Usecase #1: OmitByType

If you are a typescript dev then you most certainly have used this TS utility called [Omit](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys). It helps you to construct a new type from the existing type by removing the keys mentioned in the second parameter of `Omit`.

Take a look at our `Vehicle` example with `Omit` utility:

```tsx
type Vehicle = {
	type: 'Car',
	noOfWheels: 4,
	color: 'white'
}

// Let us Omit noOfWheels

type OmmitedVehicle = Omit<Vehicle, 'noOfWheels'>

/* 
type OmmitedVehicle = {
	type: 'Car',
	color: 'white'
} 
*/
```

Here we are removing the property `noOfWheels` from `Vehicle` i.e. we are removing properties based on a property name. What if we wanted to remove a property based on the type we pass as second argument to `Omit` 

For example, we would like to remove all the properties from `Vehicle` who’s values are string i.e. remove `type` and `color` property. 

At this moment TS doesn’t have any such utility to Omit by Type. But we can build this easily with the help of Mapped types and Generics

Let us first define what we want here. We want are utility to:

1. It should accept 2nd argument that tells the properties to remove based on type.
2. Traverse through each property of the type
3. If the value of the property is equal to the 2nd argument it should exclude it

But before we start our implementation we should take a look at how the original `Omit` utility type works:

```tsx
type Omit<T, U> = {
  [K in keyof T as Exclude<K, U>]: T[K]
}
```

It first traverses through each key of the `T` type with the help of:

```tsx
K in keyof T
```

In terms of our `Vehicle` example, K here is the property name that the mapped type will traverse i.e. `type` `noOfWheels` `color`. Next, with the help of `as` clause we remap the key such that we exclude the key K if it matches the 2nd parameter of the `Omit` Utility. Again if you see the internal working of the `Exclude`  utility provided by TS it is as follows:

```tsx
type Exclude<T, U> = T extends U ? never : T
```

This means that if `T` and `U` matches then it returns `never` or else it returns the type `T`. This also means that on the match of both the type we don’t want to return anything which is `never`. This becomes useful in the `Omit` case as follows:

If key `K` in the above mapped type matches with the 2nd parameter i.e. `U` then we should not include it in the mapped type. This case can be visualised as follows:

```tsx
type Vehicle = {
	type: 'Car',
	noOfWheels: 4,
	color: 'white'
}

type OmmitedVehicle = Omit<Vehicle, 'noOfWheels'>

/*
Internally this happens:

type OmittedVehicle = {
  ['noOfWheels' in keyof T as Exclude<'noOfWheels', 'noOfWheels'>]: T[K]
}

which truns into: 

type OmittedVehicle = {
  ['noOfWheels' in keyof T as never]: T[K]
}

and a key in mapped type that is mapped to never becomes excluded from the mapping.
Ideally this would return a blank object like `{}`
*/
```

So this gives a new type constructed by omitting properties by name. Getting this understanding was important so that you can easily grasp the concept of our utility: `OmitByType`. Here is the utility:

```tsx
type OmitByType<Type, DataType> = {
  [K in keyof Type as Exclude<K, Type[K] extends DataType ? K : never>]: Type[K]
}
```

Just watch this utility type carefully. Isn’t it pretty similar to the original `Omit` type. Yes, indeed it is but there is a small change which is in the Exclude’s 2nd parameter:

```tsx
Type[K] extends DataType ? K : never
```

Earlier in the `Omit` type we directly put `U` as the 2nd parameter to exclude. But here we are doing the following things:

1. Since we are accepting the type to omit is `DataType` in our `OmitByType` utility, we compare it with the current property’s value. Here the current property is `Type[K]` 
2. If it matches with `DataType` we pass this property K to the exclude or else we return never.

Things will get clearer when do the dry run. For the dry run let us again take the `Vehicle` example from above:

```tsx

type Vehicle = {
	type: 'Car',
	noOfWheels: 4,
	color: 'white'
}

type OmitByType<Type, DataType> = {
  [K in keyof Type as Exclude<K, Type[K] extends DataType ? K : never>]: Type[K]
}

type OmittedCarByType = OmitByType<Vehicle, string>
```

For the above give code the dry run will look like below:

| Key | Value | Value Type | Condition Check (Value Type extends string?) | Exclude<K, Condition> | Action |
| --- | --- | --- | --- | --- | --- |
| type | 'Car' | string | Yes | Exclude type | Exclude type |
| noOfWheels | 4 | number | No | Include noOfWheels | Include noOfWheels |
| color | 'white' | string | Yes | Exclude color | Exclude color |

This utility type was a solution to the following typescript challenge: https://github.com/type-challenges/type-challenges/blob/main/questions/02852-medium-omitbytype/README.md

## Usecase #2: PartialByKeys

This utility is again very similar to the Omit utility. `PartialByKeys` will take second argument as union of keys that needs to be made partial/optional in the Type argument T. 

Here is how this utility will look like:

```tsx
type Flatten<T> = {
  [K in keyof T]: T[K]
}

type PartialByKeys<T extends {}, K extends keyof T = keyof T> = [K] extends [''] ? Partial<T> : Flatten<Omit<T, K> & Partial<Pick<T, K>>>
```

Let me simplify this a bit and explain you guys the working of it:

- Here we have created a generic utility called `PartialByKeys` that take in two arguments `T` and `k`. `T` is expected to be of type object and `K` is expected to be of keys of `T` and is initialised with keys of `T` object type.
- Next, we first check that if Keys `K` are blank or not. If it is then we should return an object type who’s all the keys are optional.
    - Notice this syntax here that is used:
    
    ```tsx
    [K] extends ['']
    ```
    
    - The purpose of using square bracket here is because we don’t want to have [distributed conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types).
- If `K` keys are not blank then we should return a new object type with the specified keys as optional/partial.
    - Here we do a clever trick by separating out the keys that are not needed to be partial we keep it with the help of `Omit<T, K>`
    - The ones which we want to make partial we first construct a new object type with the keys that needs to be partial. We do that with the help of `Pick`.
        
        ```tsx
        Pick<T, K>
        ```
        
    - Next me this entire pick partial with the help of `Partial<Pick<T, K>>`.
    - Lastly we combine these both objects into one.
    - Quick note: We also make use of Flatten utility so that you can get a clear type annotation instead of a messed up partial + Pick keys

Here is the Dry run of this Utility with the `Vehicle` example:

| Key | Value | Included in Omit<Vehicle, 'color'>? | Included in Partial<Pick<Vehicle, 'color'>>? | Final Inclusion | Optional? |
| --- | --- | --- | --- | --- | --- |
| type | 'Car' | Yes | No | Yes | No |
| noOfWheels | 4 | Yes | No | Yes | No |
| color | 'white' | No | Yes | Yes | Yes |

## Usecase #3: PickByType

In `PickByType`, we pick all the properties of object type who’s value matches with the type specified in the utility as the second argument.

Here is how `PickByType` looks like:

```tsx
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P]
} 
```

The only thing that differentiates this with `OmitByType` is the way we make use of the `as` clause. Here we make use of `as` clause such that only the keys will be shown who value of `T[P]` matches with `U` i.e. second parameter. 

Here is the dry run of this utility on the `Vehicle` type:

| Key | Value | Value Type | Matches string? | Include Key in Result? |
| --- | --- | --- | --- | --- |
| type | 'Car' | string | Yes | Yes |
| noOfWheels | 4 | number | No | No |
| color | 'white' | string | Yes | Yes |

## Summary

At first typescript might look crazy difficult to understand, to follow and might look like some wizardry. But trust me it gets simpler when you learn the basics and start practicing.

So in this blog post we learned about Mapped types and its crazy ass use cases. We also learned some internal workings of the existing TS utility types such as `Omit` and `Exclude`.

Thanks a lot for reading my blogpost.

You can follow me on [twitter](https://twitter.com/keurplkar), [github](http://github.com/keyurparalkar), and [linkedIn](https://www.linkedin.com/in/keyur-paralkar-494415107/).