How I improve my skills in Typescript #2 : Mapped Type

codeoz

Code Oz

Posted on April 3, 2023

How I improve my skills in Typescript #2 : Mapped Type

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

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

If you check Usercopy type you will see this

type UserCopy = {
    name: string;
    age: number;
    userName: string;
}
Enter fullscreen mode Exit fullscreen mode

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

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 typing name: string

  • Iterate to the second propery of User : [age]: User['age'], so we get the following typing age: number

  • Iterate to the last propery of User : [userName]: User['userName'], so we get the following typing userName: 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
}
Enter fullscreen mode Exit fullscreen mode

And get something like

type UserCopy = {
    name: boolean;
    age: boolean;
    userName: boolean;
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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!

šŸ’– šŸ’Ŗ šŸ™… šŸš©
codeoz
Code Oz

Posted on April 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related