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'
}
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]
}
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'
}
*/
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:
- It should accept 2nd argument that tells the properties to remove based on type.
- Traverse through each property of the type
- 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]
}
It first traverses through each key of the T
type with the help of:
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:
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:
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 `{}`
*/
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]
}
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
Earlier in the Omit
type we directly put U
as the 2nd parameter to exclude. But here we are doing the following things:
- Since we are accepting the type to omit is
DataType
in ourOmitByType
utility, we compare it with the current property’s value. Here the current property isType[K]
- 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>
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>>>
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 argumentsT
andk
.T
is expected to be of type object andK
is expected to be of keys ofT
and is initialised with keys ofT
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 ['']
- The purpose of using square bracket here is because we don’t want to have distributed 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
.
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 we do a clever trick by separating out the keys that are not needed to be partial we keep it with the help of
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]
}
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.
Top comments (0)