An introduction to mapped types
I’ll start start with an introduction of one of TypeScript’s powerful features: mapped types before we discuss how to create “prefix and suffix” utility types.
When you don’t want to repeat yourself, sometimes a type needs to be based on another type.
Imagine you have the following type:
Pretty simple so far. Now imagine a hypothetical state management implementation.
The next step is often creating a new type or interface to model specific state related operations - namely mutations, getters, and actions. Generics are a great way to achieve concepts like
T is the type being operated on. But that still requires the creation of types for each operation. Of course, that’s assuming you aren’t using stringly typed systems.
The natural next step would be to create types such as:
What if there was a way, with the power of TypeScript, to create these types automatically, with type safety? This could aid in the construction of the hypothetical state management store.
It is possible, and this this is where mapped types come in.
A mapped type can be summised by the formula
type B = type A + transformation.
Step by step
That’s certainly, at first glance, some fairly heavy syntax. Let’s break it down to individual steps.
Creating an identical type
We start by creating almost the simplest mapped type in which the exact same properties are mapped to the new type.
As can be seen in the playground link the
NewUser has identical properties. It could possibly be a so-called “identity type” akin to an ”identity function” in the world of functional programming and mathematics.
So what have we demonstrated so far? Well, we have mapped over a type to create an identical type. It stands to reason that we should be able to transform the properties and values of the new type.
Transforming the new type
Let’s create a new mapped type where the value of each property is a function instead of the existing primitive string value. Note how the function return type can also be specified - by indexing into the generic type.
Types = Flexibility
Remember that as we are creating a new type we can define it anyway we like. The following code is only a demonstration of this ability.
Changing the property names
We’ve covered creating a new value for a property. But what about the property name itself? Looking at the original full example from earlier, we see that it is possible.
That gets us a half-way solution -
getname. This is obviously not ideal as it’s not following conventional naming conventions. TypeScript allows us to use string interpolation to create the new key name. We need a way of changing the case of characters in the string. Remember, this is happening at compile time though - not at runtime.
Intrinsic String Manipulation Types
TypeScript can also help us with changing the character casing too with Template Literal Types or more specifically Intrinsic String Manipulation Types.
We’ve now come full circle and created the
Getters<Type> from the documentation and explained each step in the process. But can we do even better and make this reusable?
🔥 Yes! 🔥
First we need to define what functionality we need to extract from the example we have being building. Currently, we have a hardcoded
get being prefixed to every key in the given type. Additionally, we appropriately change the case of characters to follow normal naming conventions of the language. That seems like an ideal candidate to extract into it’s own module. There will be times when we wish to suffix every key as well. That’s another candidate to be extracted into the new module.
Now that we have created the new module we have abstracted away the implementation details of how a mapped type can prefix/suffix values with appropriate capitalisation we can refactor the
Now that is fascinating!
A “type transformation pipeline”?
Notice how we created separate
GetterFunctions types and then effectively applied one mapped type onto another mapped type. This could keep going! This time an example with one of the existing utility types:
How I felt after discovering mapped types and then realising the ability to further compose mapped types into a pseudo-pipeline where each stage transforms the type (or rather, creates a new type) further:
We have now successfully taken the example from the documentation and taken the following actions.
- Broken down the
Gettersexample into discreet steps
- Explained the inner workings of mapped types and how to re-map keys and values
- Abstracted the prefixing of keys and introduced two reusable generic and mapped types;
- Refactored the existing code to use the new types leading to cleaner and easier to read code all while utilising the massive capabilities of TypeScript’s type system
These points make me really excited and I’m intrigued to see where I can take it next.