DEV Community

Cover image for Mastering Mapped Types in TypeScript and it's Practical Applications
Keyur Paralkar
Keyur Paralkar

Posted on • Edited on

Mastering Mapped Types in TypeScript and it's Practical Applications

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 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 and Michgan Typescript’s youtube channel. 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.

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

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:

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:

type Vehicle = {
    type: 'Car',
    noOfWheels: 4,
    color: 'white'
}
Enter fullscreen mode Exit fullscreen mode

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:

type Car = {
    [P in keyof Vehicle]?: Vehicle[P] 
}
Enter fullscreen mode Exit fullscreen mode

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. 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:

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

// Let us Omit noOfWheels

type OmmitedVehicle = Omit<Vehicle, 'noOfWheels'>

/* 
type OmmitedVehicle = {
    type: 'Car',
    color: 'white'
} 
*/
Enter fullscreen mode Exit fullscreen mode

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:

type Omit<T, U> = {
  [K in keyof T as Exclude<K, U>]: T[K]
}
Enter fullscreen mode Exit fullscreen mode

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

K in keyof T
Enter fullscreen mode Exit fullscreen mode

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:

type Exclude<T, U> = T extends U ? never : T
Enter fullscreen mode Exit fullscreen mode

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:

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

type OmmitedVehicle = Omit<Vehicle, 'noOfWheels'>

/*
Internally this happens:

type OmittedVehicle = {

}

which truns into: 

type OmittedVehicle = {

}

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

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:

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

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:

Type[K] extends DataType ? K : never
Enter fullscreen mode Exit fullscreen mode

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:


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>
Enter fullscreen mode Exit fullscreen mode

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

Key Value Value Type Condition Check (Value Type extends string?) Exclude 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:

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>>>
Enter fullscreen mode Exit fullscreen mode

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:
    [K] extends ['']
    
  • 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.

      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? Included in Partial>? 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:

type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P]
} 
Enter fullscreen mode Exit fullscreen mode

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 twittergithub, and linkedIn.

Top comments (0)