DEV Community

Cover image for Intro to Typescript generics (a must know)
Simon Ström
Simon Ström

Posted on • Updated on

Intro to Typescript generics (a must know)

Photo by Anas Alshanti (Unsplash)

If you are a Typescript developer and haven't already started using generics, you are missing out! The word itself is quite scary, 🧟generics👻. But fear not, it is really simple and very useful!

What the *!#@ is generics any way?

Since JavaScript is a dynamic language generics is not a thing there. Everything is kind of generic.

You would have go down the road to C#,Java or Tyescript to find definitions and use cases for generics.

Generics are enabling us to create a component that can work over a variety of types rather than a single one. (That line is from the Typescript docs, and you should go there later to really dig deeper...)

When creating a function, for example, some functions would be great if they could work with different types. Or really any type! And that is easy of course, just slap some :any magic there and the function is generic. Right?

Image of man with the caption: Well yes but actually no

Generic functions accept many types while remaining the type info!

One example, please!

The code can be found here

Say we have a simple interface in our game app that looks like this

interface IScore {
    playerName: string;
    kills: number;
    deaths: number;
}
Enter fullscreen mode Exit fullscreen mode

Now we need some way of calculating averages of arrays with objects of this interface. So we write something like this:

function avg(items:IScore[], getter: (t:IScore) => number) {
  const sum = items.reduce((acc, curr) => getter(curr) + acc, 0)
  return sum / items.length
}
Enter fullscreen mode Exit fullscreen mode

Simple, we take an array of scores as the first argument, and the second is a function that will get the number property used to calculate the average.

But this function only works for one type, the IScore. That is not cool, this function could be used with other types as well. As a generic function, so we could do like this, right:

function avg(items:any[], getter: (t:any) => number){/*...*/}
Enter fullscreen mode Exit fullscreen mode

Great, now it works with literally :any type. But, wait. Isn't the whole point of Typescript not to do like this. Dynamic scary things can happen now, what if we remove a property from the interface Cannot read property X of... errors will haunt us when the getter is not finding the property name.

Enter generics (the real type)

To make this function a generic function, and add type checks, we only need to alter the signature of the function. A generic type is annotated with the <> signs before the function arguments. Like this:

function avg<T>(...
Enter fullscreen mode Exit fullscreen mode

The T part is the name of your generic type. This could almost be anything, common (what I have seen at least) is to use T if there is only one type. Yes, T is for Type. You could be more specific, it's encouraged when using multiple generic types in one function like <TFriend, TEnemy>

The next step is to simply replace the :any parts with :T to make the function generic:

function avg<T>(items:T[], getter: (t:T) => number){/*...*/}
Enter fullscreen mode Exit fullscreen mode

Now we could just do something like this

const deathAvg = avg<IScore>(scores, x => x.deaths)
Enter fullscreen mode Exit fullscreen mode

Or even this:

const deathAvg = avg(scores, x => x.deaths)
Enter fullscreen mode Exit fullscreen mode

Typescript will infer the type to use in the generic function, and use that one automatically

Extra credits

While this function could actually use any type given to it (a string array could calculate the average char length of all words) we can for the sake of example restrict this one to only accept arrays of objects. This would be called a constraint on the generic type.

Meaning, the generic type cannot be anything. It must fulfill some rule(s). This information is added to the generic signature with the keyword extends. We say the given type T must extend something. In our example object. So we just change to this:

function avg<T extends object>(items:T[]...
Enter fullscreen mode Exit fullscreen mode

One more example?

You could also make your types generic. A lot of API endpoints response types have the same basic shape. They contain information about the total number of items in the database, response, and if there is more data to fetch. And then of course the list of items. So I could write a type collection for everything like this:

interface IUsersCollection {
  values: IUser[];
  totalInDatabase: number;
  totalInResponse: number;
  isLast: boolean
}

interface IDatabaseCollection {
  values: IDatabase[];
  totalInDatabase: number;
  totalInResponse: number;
  isLast: boolean
}

Enter fullscreen mode Exit fullscreen mode

That is not fun. Not at all. Think only if we add one property to the response...

Simply do this

interface ICollection<TType> {
  values: TType[];
  totalInDatabase: number;
  totalInResponse: number;
  isLast: boolean
}
interface IUsersCollection extends ICollection<IUser> {}

interface IDatabaseCollection extends ICollection<IDatabase> {}

Enter fullscreen mode Exit fullscreen mode

And it works great with type as well:

type Collection<TType> = {
  values: TType[];
  /* etc. */
}
Enter fullscreen mode Exit fullscreen mode

And you could just put your type definition straight there

type UsersCollection = Collection<{
  id: number;
  name: string;
  /* etc.*/
}>
Enter fullscreen mode Exit fullscreen mode

Summary

So generics, saves you time both in making smarter types/interfaces and implementing type safety where you would otherwise just mess around with :any.

And this is not all. Generics can be used in several other fun and useful ways!

Top comments (0)