Mastering TypeScript: How Mapped Types Can Streamline Your Code and Prevent Bugs
Introduction
We've all been there: you create a new fancy helper method for object operations, eager to change the world with it.
Let's take a look at it:
function sumFields(object: object, properties: any[]){
[...]
}
beautiful innit?
However, it has some major flaws: the object can essentially be anything, and properties can be of any type. As soon as the user tries to pick properties that cannot be added the program will crash and the world will burn... or at least you'll have a bad Friday afternoon debugging that.
What if we could somehow ensure that our users (in this case other developers) pass only the correct values there without building unnecessary validation pipeline?
Entering: TypeScript mapped types
TypeScript's mapped types are a powerful tool, allowing you to create types based on another type. Here's a simple demo straight from the TypeScript docs:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type Features = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<Features>
// ^? type FeatureOptions = { darkMode: boolean; newUserProfile: boolean; }
As you can see the OptionsFlags
mapped type turned all of the properties into booleans. Pretty cool! 😅
Now, let's use this approach to solve our issue:
Picking properties from object based on their type using mapped types
Let's assume we have some kind of type, for example
type Employee = {
name: string,
age: number,
salary: number
}
How could we extract only numeric values from it?
We need two ingredients:
Mapped types
Conditionals
Quick typescript type-level conditional primer:
type IsNumber<T> = T extends number ? true : false
Let's now combine the two and create our beautiful monstrosity
type NumericOnly<TObject extends object> = {
[Key in keyof TObject as TObject[Key] extends number ? Key : never]: TObject[Key]
}
I know I know. This looks scary but it really isn't, let's break it down step by step 😀
Defining the Generic Type:
- First, we define a new type,
NumericOnly
. It takes a generic parameter,TObject
, which represents the object we'll be working with."
- First, we define a new type,
Ensuring TObject Is an Object:
- The
TObject
parameter extends fromobject
, ensuring that any value passed toNumericOnly
must be an object.
- The
Mapping Over Object Keys
- We then map over the keys of
TObject
. This is done using TypeScript's key mapping syntax, where we iterate over each key (Key
) inTObject
.
- We then map over the keys of
Applying a Conditional Type
- For each key, we apply a conditional type check. If the type of
TObject[Key]
is a number, we keep the key in the resulting type. Otherwise, we replace it withnever
, effectively filtering it out.
- For each key, we apply a conditional type check. If the type of
Preserving Property Types
- Finally, on the right side of our mapped type, we maintain the original types of the properties. This means if a key passes our number check, its type in
NumericOnly
remains the same as inTObject
.
- Finally, on the right side of our mapped type, we maintain the original types of the properties. This means if a key passes our number check, its type in
Let's see what this produces when we wrap Employee
type with it:
type Employee = {
name: string,
age: number,
salary: number
}
type NumericOnly<TObject extends object> = {
[Key in keyof TObject as TObject[Key] extends number ? Key : never]: TObject[Key]
}
type Result = NumericOnly<Employee>
// ^? type Result = { age: number; salary: number; }
Woah 😮 we managed to separate the numeric properties from the main object.
We can now turn this newly created type into union type by utilizing keyof
operator
type NumericOnlyKeys<T extends object> = keyof NumericOnly<T>
type Result = NumericOnlyKeys<Employee>
// ^? type Result = "age" | "salary"
Do you now see how this will be useful in our final demo? 😅
Turning our function type-safe using mapped types and conditionals
Returning to our starter example:
function sumFields(object: object, properties: any[]){
[...]
}
Bringing our helper methods on board
type NumericOnly<TObject extends object> = {
[Key in keyof TObject as TObject[Key] extends number ? Key : never]: TObject[Key]
}
type NumericOnlyKeys<T extends object> = keyof NumericOnly<T>
function sumFields(object: object, properties: any[]){
[...]
}
All that's left to do is to make our function generic
function sumFields<T extends object>(object: T, properties: NumericOnlyKeys<T>[]){
[...]
}
Demo:
As you can see only numeric properties are now allowed in our parameter array and we're (type)safe!
Thank you for sticking around. I hope you found my type-level shenanigans enlightening! :)