DEV Community

Cover image for Mastering TypeScript: How Mapped Types Can Streamline Your Code and Prevent Bugs
wiatr.dev
wiatr.dev

Posted on

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[]){
    [...]
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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

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

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

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

I know I know. This looks scary but it really isn't, let's break it down step by step 😀

  1. 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."
  2. Ensuring TObject Is an Object: ‧ The TObject parameter extends from object, ensuring that any value passed to NumericOnly must be an object.
  3. 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) in TObject.
  4. 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 with never, effectively filtering it out.
  5. 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 in TObject.

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

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

Do you now see how this will be useful in our final demo? 😅

Turning our function type-safe using mapped types and conditionals

Image description

Returning to our starter example:

function sumFields(object: object, properties: any[]){
    [...]
}
Enter fullscreen mode Exit fullscreen mode

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[]){
    [...]
}
Enter fullscreen mode Exit fullscreen mode

All that's left to do is to make our function generic

function sumFields<T extends object>(object: T, properties: NumericOnlyKeys<T>[]){
    [...]
}
Enter fullscreen mode Exit fullscreen mode

Demo:

As you can see only numeric properties are now allowed in our parameter array and we're (type)safe!

💡 Playground demo: link

💡 Read more about mapped types: link

Thank you for sticking around. I hope you found my type-level shenanigans enlightening! :)

Top comments (0)