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?
Generic functions accept many types while remaining the type info!
One example, please!
Say we have a simple interface in our game app that looks like this
interface IScore {
playerName: string;
kills: number;
deaths: number;
}
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
}
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){/*...*/}
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>(...
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){/*...*/}
Now we could just do something like this
const deathAvg = avg<IScore>(scores, x => x.deaths)
Or even this:
const deathAvg = avg(scores, x => x.deaths)
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[]...
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
}
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> {}
And it works great with type
as well:
type Collection<TType> = {
values: TType[];
/* etc. */
}
And you could just put your type definition straight there
type UsersCollection = Collection<{
id: number;
name: string;
/* etc.*/
}>
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)