DEV Community

Cover image for 4 Ideas of how to harness the power of Typescript generic function
Gleb Irovich
Gleb Irovich

Posted on

4 Ideas of how to harness the power of Typescript generic function

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 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 or lodash. 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

Before we start

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

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

1. Map by key

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

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:

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

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. 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.

interface ItemGroup<T> {
  [key: string]: T[];
}
Enter fullscreen mode Exit fullscreen mode
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
  }, {})
}
Enter fullscreen mode Exit fullscreen mode

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.

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

3. Merge

function merge<T extends Item, K extends Item>(a: T, b: K): T & K {
  return {...a, ...b};
}
Enter fullscreen mode Exit fullscreen mode

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

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

4. 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.

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

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.

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

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 πŸš€for more exciting content about web development.

Top comments (9)

Collapse
 
titoasty profile image
nico-boo

In sortBy, wouldn't it be better to write

[...array].sort

instead of

[...array.sort

to prevent mutation?

Great article!

Collapse
 
glebirovich profile image
Gleb Irovich

That’s a great catch! thank you πŸ™ I will fix it

Collapse
 
jwp profile image
John Peters

Gleb;
What would T become if we didn't make the = any assignment?

interface Item<T = any> {

}
Collapse
 
glebirovich profile image
Gleb Irovich

Then β€˜T’ will be a mandatory parameter, you will have to specify. It works very similar to normal js functions.

Collapse
 
jwp profile image
John Peters

Mandatory but morphs into what was passed in right?

Thread Thread
 
glebirovich profile image
Gleb Irovich

I am not sure if I got the question. But I will try to give a better example.
Let's say you have a generic interface:

interface Item<T> {
  value: T;
}

const item1: Item<string> = { value: "Gleb" }

interface Person {
  name: string;
  age: number;
}

const item2: Item<Person> = { value: { name: "Gleb", age: 27 } }

const item3: Item = {} // Error: Generic type 'Item<T>' requires 1 type argument(s)

If you set a default value of the generic type, item3 will not cause an error. It will set the type of value to any.
You can use any type as a default:

interface Item<T = number> {
  value: T; // Will be number, if no other type is passed to the interface
}

const item: Item = { value: 1 }

Does it answer your question?

Thread Thread
 
jwp profile image
John Peters

Yes I think I saw no value in this:

interface Item<T = any> {

}
Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

I really fail to see what benefit TypeScript gives you. The same could be written in simpler JS very easily. It just feels like TypeScript is a crutch for developers coming from strictly typed languages who are unwilling to modify their habits

Collapse
 
glebirovich profile image
Gleb Irovich

When I started with TS, it was a React project, so I was not convinced at all, especially given the good old PropTypes lib for checking react props.
However now typescript is my tool of choice and I think it’s not fair to compare what you can or cannot do with JS instead. You can do anything without TS and with TS you can only go as fas as JS would allow.
Here are the main selling points of TS from my point of view:

  • powerful refactoring tools
  • great assistance while writing the code
  • I like a blueprint - first approach, which encourages to think about your data and then writing actual code
  • self-documented code. No comments needed when you can read types and function signatures