How I improve my skills in Typescript #2 : Mapped Type
Code Oz
Posted on April 3, 2023
I will share with us some tips that improved my skill in Typescript ! Today we will learn how to use Mapped Type
!
What is and why use Mapped Type ?
Using mapped type
is usefull when :
need to create a derived type from another type and keep both sync
Let's check an exemple to understand !
type User = {
name: string
age: number
userName: string
}
type UserPermissions = {
canUpdateName: boolean
canUpdateAge: boolean
canUpdateUserName: boolean
}
Here we can see similar thing between User
& UserPermissions
type. Is there any solutions to link both type ? Because we don't want to add manually property in UserPermissions
every time we add a new property in User
type.
The answer is yes
and the solution is named Mapped type
!
Mapped type: The basics
Create the same type
Before answering to the issue above, let's see how Mapped type is working !
We will create a type from User
type, to begin let's create the EXACT SAME type.
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
[Property in keyof User]: User[Property]
}
If you check Usercopy
type you will see this
type UserCopy = {
name: string;
age: number;
userName: string;
}
But why we are doing this, we create the same type as User
?!
I choose a basic example to see you how Mapped type is working. I will try to explain you what the f*ck is going on.
type UserCopy = {
[Property in keyof User]: User[Property]
}
First We are iterate each key in the User
type with the in keyof
operator (frequently used when using mapped type), each key is contains in the variable Property
('name'
, 'age'
, 'userName'
)
After this, we assign the value of each property with the dedicated value in User[Property]
. Visually we are gonna something like:
1) Start : [Property in keyof User]: User[Property]
Iterate to the first propery of User :
[name]: User['name']
, so we get the following typingname: string
Iterate to the second propery of User :
[age]: User['age']
, so we get the following typingage: number
Iterate to the last propery of User :
[userName]: User['userName']
, so we get the following typinguserName: string
2) After iterating on each key, we get a new Type that is the same type as User
!
Edit value from type to type
We can also edit the property value type like :
type UserCopy = {
[Property in keyof User]: boolean
}
And get something like
type UserCopy = {
name: boolean;
age: boolean;
userName: boolean;
}
We begin to see the power of Mapped type ! If we are adding a new property in User
type, it will be automatically added to the UserCopy
type without doing anything ! Don't forget, developer are Lazy ;)
Mapped type: mapping modifier
Let's play with Mapped type from now !
We will use mapping modifier
on mapped type.
If I need to create the folowing type UserReadonlyProperty
that is the User
type with readonly
flag on each keys. I can create something like.
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
readonly [Property in keyof User]: User[Property];
};
// type UserCopy = {
// readonly name: string;
// readonly age: number;
// readonly userName: string;
// }
Wow it's interesting ! We direclty assign readonly
flag on each key ! We can also do the opposite thing ! We just need to use -
before readonly to remove all readonly flag from type ~
type User = {
readonly name: string
readonly age: number
readonly userName: string
}
type UserCopy = {
-readonly [Property in keyof User]: User[Property];
};
// We Get this š
type UserCopy = {
name: string;
age: number;
userName: string;
}
This is called mapping modifier
and we have another mapping modifier that I need to show you, the Optional
mapping modifier (?:
).
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
[Property in keyof User]?: User[Property];
};
// We Get this š
type UserCopy = {
name?: string | undefined;
age?: number | undefined;
userName?: string | undefined;
}
As you can see we can add 'readonly' and 'optionnal' flag on key but we can also remove them ! By using the (-
) flags: [Property in keyof User]-?: User[Property]
type User = {
name?: string
age?: number
userName: string
}
type UserCopy = {
[Property in keyof User]-?: User[Property];
};
// We Get this š
type UserCopy = {
name: string;
age: number;
userName: string;
}
And finally we can combine both š„
type User = {
name: string
age: number
userName: string
}
type UserCopy = {
readonly [Property in keyof User]?: User[Property];
};
// We Get this š
type UserCopy = {
readonly name?: string | undefined;
readonly age?: number | undefined;
readonly userName?: string | undefined;
}
Mapped type: Key remaping with as
We see the mapping modifier
, that is a very nice feature combined with mapped type ! So now let's go more deeper in mapped type
Literals string
We can remaping key using literals string !
type User = {
name: string
age: number
userName: string
}
type RenameKey<Type> = {
[Property in keyof Type as `canUpdate${string & Property}`]: Type[Property]
}
type UserCopy = RenameKey<User>
// We Get this š
type UserCopy = {
canUpdatename: string;
canUpdateage: number;
canUpdateuserName: string;
}
Use as
permits to change the key value and keep it in the new value !
Let's destructure the synthax step by step.
1) We iterate through Type
key's, so we get name
, age
and userName
.
2) In each iteration, we rename the key value as canUpdate${Property}
, so we are getting
canUpdatename
canUpdateage
canUpdateuserName
2.5) If we pay attention on the synthax, we can see the {string & Property}
synthax. Why are we needing to use string
value here ? I will create a **dedicated article on this, but to summarize it, we need to tell to TS that the key is a string
and not a symbol
, because we cannot use symbol
in literals string (other key type like number of bingint will be corce to string)
As you can see, remaping key is very powefull combined to mapped type. But there is a "little" issue here, we get canUpdatename
not canUpdateName
.
How can we achieve this ? š§
Intrinsic string
To help with string manipulation, TypeScript includes a set of types which can be used in string manipulation and we are going to use it now in order to transform canUpdatename
to canUpdateName
.
type User = {
name: string
age: number
userName: string
}
type Copy<Type> = {
[Property in keyof Type as `canUpdate${Capitalize<string & Property>}`]: Type[Property];
};
type UserCopy = Copy<User>
// We Get this š
type UserCopy = {
canUpdateName: string;
canUpdateAge: number;
canUpdateUserName: string;
}
We do it ! Use intrinsic string
allow us to Capitalize string value ! Here we capitalize the Key value from User
type.
We can also use intrinsic string
out of mapped type context.
type LowercaseGreeting = "hello, world"
type Greeting = Capitalize<LowercaseGreeting> //Hello, world
There are other Intrinsic string here
Type utils
I repeat it again, but Mapped type
are powerfull, we can rename each key from a type to create another type, we can use intrinsic string
to manipulate string.
There is another things to do with them ;)
We can use Type Utils ! (if you don't know what is this, check my last article about Typescript tips or go here)
If we need to exclude a set of keys from a type to create a transformed type, we can use mapped type
combined with Type Utils !!
type User = {
name: string
age: number
userName: string
}
type CopyWithoutKeys<Type, Keys> = {
[Property in keyof Type as Exclude<Property , Keys>]: Type[Property];
};
type UserCopyWithoutNameAndUsername = CopyWithoutKeys<User, 'name' | 'userName'>
// We Get this š
type UserCopyWithoutNameAndUsername = {
age: number;
}
I think you get the Mapped type synthax and concept. And of course we can use all features that we seen before !
Bonus : Get all values type from a Mapped type
A bonus for all people that read until this section.
You can get all key's values type from mapped type (and type)
type User = {
name: string
age: number
userName: string
isAdmin: boolean
}
type CopyWithoutKeys<Type> = {
[Property in keyof Type]: Type[Property];
};
type UserCopy = CopyWithoutKeys<User>
type UserCopyValueTypes = UserCopy[keyof UserCopy] // string | number | boolean
Note that I added isAdmin
key without adding it in UserCopy
type, this is the magic of Mapped Type
;)
Advanced concept with Mapped type
Never as value in key/pair
Let's see what happened when you use Never as key/value with mapped type !
Never as key with mapped type
If you add a condition during key iteration when using mapped type with a never
case, it will produce something cool
type isA<T> = T extends 'a' ? never : T
type To<T> = {
[P in keyof T as isA<P>]: T[P]
}
type Toto = To<{ a: 1; b: 2; c: 3 }> // { b: 2; c: 3; }
When you set a key with the never
value, it will be Omit from the final type !
Never as value with mapped type
As we seen before, using never
can lead to some usefull tricks about mapped type, but what is happening if you are using never
as value ?
type To<T> = {
[P in keyof T]: T[P] extends never ? never : P
}
type Toto = To<{ a: 1; b: 2; c: never }> // {a: "a" b: "b" c: never;}
As you can see, never
as value is not Omit
like never
as key.
Use never
as value can be used to remove some key/value depending on a condition, so if you need to do this, use your condition at key
level !
// type PropertyKey = string | number | symbol
type isA<T extends Record<PropertyKey, unknown>, P extends PropertyKey> = T[P] extends 1 // Your condition
? never : P
type To<T extends Record<PropertyKey, unknown>> = {
[P in keyof T as isA<T,P>]: T[P]
}
type Toto = To<{ a: 1; b: 2; c: 2 }> // {b: 2;c: 2;}
In the example above, we remove all key/value with a value equal to 1, we add the condition at the key
level, so the never
value will be automatically Omit
!
Union type with value
If you have any issue about this kind ot error
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
const redComponent = palette.red.at(0); // Property 'at' does not exist on type 'string | RGB'.
You should read my last article about satisfies
operator !
I hope you like this reading!
āļø You can SUPPORT MY WORKS š
šāāļø You can follow me on š
š Twitter : https://twitter.com/code__oz
šØāš» Github: https://github.com/Code-Oz
š«š·š„ For french developper you can check my YoutubeChannel
And you can mark š this article!
Posted on April 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.