4 Ideas of how to harness the power of Typescript generic function
Tags: typescript, javascript, beginners, tutorial
Published: 2020-06-29 19:15:41

Typescript is a powerful tool that significantly improves the reliability of the javascript code. However, it also adds particular overhead, that developers have to deal with while working with Typescript.

Generic functions are, probably, one of the trickiest but though most powerful concepts of Typescript. In [my previous post](https://dev.to/glebirovich/5-typescript-features-that-will-improve-your-codebase-hok) I briefly touched the topic generics, however, now I would like to dig deeper, and talk about how we can harness the power of generics to deliver scalable and reusable code. Today we will consider four ideas of generic helper functions made with ❤️and powered by Typescript.

### Disclaimer

If you are looking for an ultimate solution with a lot of different methods, you might be interested in checking out great existing libraries such [ramda](https://ramdajs.com/) or [lodash](https://lodash.com/). The purpose of this post is to discuss some examples, which I find useful in everyday development, and which are suitable for the illustration of Typescript generics. Feel free to add your use-cases in the comments, let's discuss them together 💪

### Table of content

- [Map by key](#mapByKey)
- [Group by key](#groupByKey)
- [Merge](#merge)
- [Sort](#sortBy)

### Before we start

For the sake of example, I came up with two simple interfaces and created arrays out of them.

```jsx
interface Book {
  id: number;
  author: string;
}

interface Recipe {
  id: number;
  cookingTime: number;
  ingredients: string[];
}

const books: Book[] = [
  { id: 1, author: "A" },
  { id: 2, author: "A" },
  { id: 3, author: "C" }
]

const recipes: Recipe[] = [
  { id: 1, cookingTime: 10, ingredients: ["salad"] },
  { id: 2, cookingTime: 30, ingredients: ["meat"] }
]
```

### 1. <a name="mapByKey"></a>Map by key

```tsx
interface Item<T = any> {
  [key: string]: T
}

function mapByKey<T extends Item>(array: T[], key: keyof T): Item<T> {
  return array.reduce((map, item) => ({...map, [item[key]]: item}), {})
}
```

Let's look closer to what happens here:

1. `interface Item<T = any> { ... }` is a generic interface, with a default value of `any` (yes you can have default values in generics 🚀)
2. `<T extends Item>(array: T[], key: keyof T)` : Type `T` is inferred from the parameter, but it must satisfy the condition `<T extends Item>`  (in other words `T` must be an object). 
3. `key: keyof T` second parameter is constrained to the keys which are only available in `T`. If we are using `Book`, then available keys are `id | author`.
4. `(...): Item<T>` is a definition of the return type: key-value pairs, where values are of type `T`

Let's try it in action:

```tsx
mapByKey(books, "wrongKey") // error. Not keyof T -> (not key of Book)

mapByKey(books, "id") // {"1":{"id":1,"author":"A"},"2":{"id":2,"author":"A"},"3":{"id":3,"author":"C"}}
```

As you can see, we can now benefit from knowing in advance available keys. They are automatically inferred from the type of the first argument. Warning: this helper is handy with unique values like ids; however, if you have non-unique values, you might end up overwriting a value which was previously stored for that key.

### 2. <a name="groupByKey"></a>Group by key

This method is beneficial, if you need to aggregate data based on a particular key, for instance, by author name.

We start by creating a new interface, which will define our expected output.

```tsx
interface ItemGroup<T> {
  [key: string]: T[];
}
```

```tsx
function groupByKey<T extends Item>(array: T[], key: keyof T): ItemGroup<T> {
  return array.reduce<ItemGroup<T>>((map, item) => {
    const itemKey = item[key]
    if(map[itemKey]) {
      map[itemKey].push(item);
    } else {
      map[itemKey] = [item]
    }

    return map
  }, {})
}
```

It's interesting to note, that `Array.prototype.reduce` is a generic function on its own, so you can specify the expected return type of the reduce to have better typing support.

In this example, we are using the same trick with `keyof T` which under the hood resolves into the union type of available keys.

```tsx
groupByKey(books, "randomString") // error. Not keyof T -> (not key of Book)
groupByKey(books, "author") // {"A":[{"id":1,"author":"A"},{"id":2,"author":"A"}],"C":[{"id":3,"author":"C"}]}
```

### 3. <a name="merge"></a>Merge

```tsx
function merge<T extends Item, K extends Item>(a: T, b: K): T & K {
  return {...a, ...b};
}
```

In the merge example `T & K` is an intersection type. That means that the returned type will have keys from both `T` and `K`.

```tsx
const result = merge(books[0], recipes[0]) // {"id":1,"author":"A","cookingTime":10,"ingredients":["bread"]}
result.author // "A"
result.randomKey // error
```

### 4. <a name="sortBy"></a>Sort

What is the problem with `Array.prototype.sort` method?  → It mutates the initial array. Therefore I decided to suggest a more flexible implementation of the sorting function, which would return a new array.

```tsx
type ValueGetter<T = any> = (item: T) => string | number;
type SortingOrder = "ascending" | "descending";

function sortBy<T extends Item>(array: T[], key: ValueGetter<T>, order: SortingOrder = "ascending") {
  if(order === "ascending") {
    return [...array].sort((a, b) => key(a) > key(b) ? 1 : -1 )
  }
  return [...array].sort((a, b) => key(a) > key(b) ? -1 : 1 )
}
```

We will use a `ValueGetter` generic function, which will return a primitive type: string or number. It is a very flexible solution because it allows us to deal with nested objects efficiently.

```tsx
// Sort by author
sortBy(books, (item) => item.author, "descending")

// Sort by number of ingredients
sortBy(recipes, (item) => item.ingredients.length)

// Sort very nested objects
const arrayOfNestedObjects = [{ level1: { level2: { name: 'A' } } }]
sortBy(arrayOfNestedObjects, (item) => item.level1.level2.name)
```

### Summary

In this post, we played around with generic functions in Typescript by writing helper functions for common operations with JS arrays and objects. Typescript provides a variety of tools to produce reusable, composable and type-safe code, and I hope you are enjoying to explore them with me!

If you liked my post, please spread a word and [follow me on Twitter](https://twitter.com/gleb_irovich) 🚀for more exciting content about web development.