Making sense of TypeScript generics

Clément Gateaud
JobTeaser Engineering
7 min readJul 4, 2024

TypeScript significantly improved JavaScript development with enhanced code reliability and maintainability, ensuring fewer runtime errors and greater productivity for developers.

But when you start working with it, the syntax can sometimes feel overwhelming.

One of the most intimidating features in TypeScript is “generics”. Look at the example below 🤯 aren’t you afraid ?

Types declarations from @tanstack/react-query (library) that can feel overwhelming

But don’t worry, we will go step by step and once you understand generics, you’ll see how powerful they are.

The best any alternative

Imagine this very simple JavaScript function that returns the first element of an array. (I agree, this function is not very useful, but it’s for the sake of explanation).

How could we type it? 🤔 What would be the type of the array? Well, in this function it could be an array of anything (a string, a number, an object). So it could be tempting to type it like this, using the type whose name must not be spoken: any 😱

But this is very bad typing. Why? Because TypeScript won’t be able to properly infer the return type of this function.

So maybe we could specify all the different types it could be?

Not a good idea. First, because it would force you to maintain a huge list of all the possible types for this function. But more importantly, because it doesn’t solve our inference issue. TypeScript is not smart enough to guess the type when calling the function.

So maybe we could use casting with as to help TypeScript?

But that’s cheating! With this type assertion, you are telling TypeScript: “Stop type checking and trust me, I know what I’m doing”. This weakens type safety which is the core purpose of using TypeScript.

Don’t do that! There’s a way safer and cleaner way to type this.
Guess how it’s named? ✨ Generics✨

Introducing generics

Let’s keep our previous example.

What we want to tell our function is: “You will take as a parameter an array of SOMETHING, and you will return a SOMETHING element”.

That’s exactly what generics are all about.

It will look like this:

Let’s zoom into parts of this code:

🟣 Instead of typing our parameter as any[] like before, we define it using a generic: we will name it SomeType. A generic is not a type itself but a type parameter, a placeholder for a type that will be specified when the function is called. You can name it whatever you want (as long as it's not a reserved keyword or an already imported type name). You will see that we often use a single letter for generics, like T. We are telling Typescript “the function will take as parameter an array of some type”.

🟢 This specifies the return type of the function. We are telling Typescript “the function will return an element of the same type as the elements in the parameter’s array”.

🟡 This part just before the function parentheses is listing the generics we are about to use for this function, inside angle brackets. Here we will only be using one generic (SomeType), but I’ll show you later that we can have multiple.

How we typed the function using a generic just means: “The parameter is an array of some type, and the function will return something of this same type”.

A more common way of writing this function would be with T (so that the syntax looks less cumbersome).

I hope this introduction could help you grasp the essence of generics.

Generics are everywhere

If you’ve already worked with Typescript, you’ve probably used generics without even knowing it. For example, consider the querySelector function in the DOM API:

In this example, querySelector uses generics to ensure that the returned element is of type HTMLElement. This type information allows TypeScript to provide accurate autocompletion and type checking for the returned element. In TypeScript, native types like HTMLElement are defined through type declarations in the standard library, specifically in lib.dom.d.ts.

Here’s what using the generics allows you to do with the element:

Here’s the difference without using generics:

Without the generic type, TypeScript would not know the specific type of element, you would not get helpful autocompletion and you would get an error (fixable with casting, but still not a good idea).

Using generics allows TypeScript to understand exactly what kind of element you’re working with, providing better type safety and developer experience. So it’s no surprise generics are widely used in native APIs and libraries.

Going deeper with generics

Generics are not limited to simple functions. They can be used in various ways to create more flexible and reusable code.

Let’s take a new example that is closer to a real-world issue than just getting the first element of an array. We’ll work with API responses.

Let’s take this code:

It defines an ApiResponse type that will always return a number status, a string message, and some data that can be anything.

But as we saw in the first part of this article, using any is not a good idea as it prevents TypeScript from inferring our types properly.

Let’s use generics to improve that:

Let’s zoom a bit on this code.

🟡 First, you notice a new syntax here. It’s a generic type. We are defining a type that is using a generic. It works kind of the same as the function example we saw before:

  • We are listing the generics used, inside angle brackets
  • We are using this generic inside the type definition

🟣 So now, every time we will use this type, we will have to precise in angle brackets what is the type of T. We are specifying that the data of our API response for this fetch will be of type { name: string; age: number }. This is a type argument.

And now, as our fetchData function is well typed, TypeScript will be able to properly infer it’s return type.

Constraining generics

Sometimes, you want your generic to not accept any type, but to have more constraints.

Keeping our example of API response, we might want our data to always be an object and nothing else.

This is possible with the extends keyword.

If we try to call the function with something else than an object as a type argument, TypeScript will throw an error.

We can even go further by constraining an object with specific properties. Let’s say we want our API data to be an object that always contains an id:

Default types for generics

Another cool feature of generics is the ability to give them a default type.

In all the examples we saw so far, we had to precise the type argument when using the generic type, like: ApiResponse<{ name: string; age: number }> .

But if you often use the same type for your generic, you can specify a default value in the type definition, with =.

You can even combine it with a constraint:

Multiple generics

Sometimes, one generic is not enough, and you need 2 different ones (or more).

Inside the angle brackets, you can just list all the generics you will use, separated with commas. And when you call a function, do the same for your types arguments.

Let’s see an example:

Generics can be intimidating at first, but once you grasp how they work, you quickly realize their power and understand why they are at the cornerstone of the TypeScript ecosystem.

By allowing functions and components to operate with a variety of types while maintaining strong type safety, TypeScript generics significantly enhance our ability to write robust and maintainable code, while making the developer experience fantastic.

Happy coding!

--

--