Mastering TypeScript: How Mapped Types Can Streamline Your Code and Prevent Bugs
Tags: javascript, webdev, typescript, programming
Published: 2023-11-14 22:59:28

### 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:

```typescript
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.*


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flld62zciwp2elvxgj5c.png)

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](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html):

```typescript
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

```typescript
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:**

```typescript
type IsNumber<T> = T extends number ? true : false
```

Let's now combine the two and create our beautiful monstrosity

```typescript
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 😀

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:

```typescript
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

```typescript
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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dv07knov6vmy3n3ygpx5.png)

Returning to our starter example:

```typescript
function sumFields(object: object, properties: any[]){
    [...]
}
```

Bringing our helper methods on board

```typescript
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

```typescript
function sumFields<T extends object>(object: T, properties: NumericOnlyKeys<T>[]){
    [...]
}
```

### **Demo:**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1700001812048/f8573ee3-e588-402c-b492-c4bc4105971b.png align="center")

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

> 💡 Playground demo: [link](https://www.typescriptlang.org/play?#code/C4TwDgpgBAcgrgWwgJwJYGMDyA7ANiAHgBVMAjAKwnWCggA9gJsATAZygHsKrgA+KALxQA3gCgoEqAG0A0hBBRU2KAGt5HAGZQS3alACG7HZWqz5AXVoMmbKNkSkUUAPxQ5CgFx2IANxTmvYx4zEHNRAF9RUVBIWEQUDBx8d1ZiK0YWdi4TPkFVdS14JDQsPEIiXiiNOGxqVA5lVkQAMVQIXDY0+gzbbJ5eAAo+6kCAGigwZA5IZGA21i8ihNLk+VSKqXMAShFxKAB6fahSOFYlCFZ2XA4AcwwoAAsUaA8AEQio9AbWGhnWBryYgk+huEC8ACYAIyjPZPVA3B7ALyQgAcADYYRJsPokF4AOQAWX06Da5DxH1ETQQrXabAGfwa4ykeJBEDx4zxcIRwDx2yiVJpHVY9JQ-2wTJZoPZUDx2KQvK2QA)


> 💡 Read more about mapped types: [link](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)


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