Skip to content

Jonathan Wilkinson

A Simple Introduction to Generics

TypeScript, Generics3 min read

At first glance generics can be daunting. If you’ve ever peeked into your favourite library’s type definitions and seen a sea of Ts and Us where they don’t seem to belong, you’d be forgiven for relegating generics to the dark arts of programming and vowing to turn your back on them forever.

But the truth is: generics are your friend. They help us write code which is “generic”: code which we can reuse in different scenarios. And at their core they’re relatively simple to use.

In this post we’ll go over the problem that generics solve and how to start using generics in your code.

The problem

Imagine you needed a function that gets the last number from an array of numbers. That’s easy enough:

1const test: number = 2;
2const getLast = (arr: number[]): number => arr[arr.length - 1];

But what if we also needed to get the last string from an array of strings? We could write another function, but this would mean duplicating our code.

The easy way out

The easiest solution is to use the any type.

1const getLast = (arr: any[]): any => arr[arr.length - 1];

Now we can pass in any type of array and get the last value.

It works. But our returned value will be typed as any, and this is generally a bad sign in TypeScript. Why? Because anything goes with any. We could do something like this:

1const last = getLast([1,2,3]);
2last.toLowerCase();

When this code runs it will throw an error: last is a number and toLowerCase isn’t an available method on a number. But TypeScript won’t pick this up. All that TypeScript sees is a variable which is type any, and because any can have any methods it can’t see that this call is doomed to fail.

Using any is a solution. But it means losing out on the very reason we’re using TypeScript - types.

The Solution

Enter generics. Generics are a way of writing type safe code which is adaptable to many different types. If we rewrote our function with generics it would like this:

1const getLast = <T>(arr: T[]): T => arr[arr.length - 1]

Our function now takes an array of T and returns a single T. Here, T doesn’t refer to a specific type: T is a type variable. Type variables are used to represent types which will be assigned at a later point in time (in this case, when we call the function).

We declare type variables in-between <>. After that they can be used just like any other types.

1// Declare type variable
2// v
3const getLast = <T>(arr: T[]): T => arr[arr.length - 1]
4// ^ ^
5// Use type variable

When we call our function, we will provide a type that T will represent. We do this by supplying the type in-between <>.

1// Tell TypeScript that T is a string
2// v
3const last = getLast<string>([‘hello’, ‘world’]);
4// ^
5// Will be correctly typed

Here we’re assigning T a type of string. TypeScript will now interpret the signature (arr: T[]) => T as (arr: string[]) => string and the return value will now be properly typed.

In this case, we’ve explicitly told TypeScript that T is assigned the type of string. But when using generics in functions, TypeScript can often work out what type to assign a type variable based on its use. We could rewrite our example as:

1const last = getLast([‘hello’, ‘world’]);

TypeScript sees that we’re passing T[] as string[] and is able to infer that T is assigned to string.

Multiple Type Variables

You can declare as many type variables as you need. As with single variables, they’re declared between <>.

1// Declare two type variables
2// v v
3Const merge = <T, U>(a: T[], b: U[]): (T|U)[] => [...a, ...b];
4// ^ ^ ^ ^
5// Use type variables like any other types

This function will take any two arrays and merge them into one. The output will be an array of a union of the two.

1merge([1], [string]); // (a: number[], b: string[]) => (string|number)[]
2merge([1], [2]); // (a: number[], b: number[]) => number[]

Not Just Functions

Although we’ve focused on functions, generics can also be used in types, interfaces and classes. Like with functions, we need to declare any type variables inside <>. Afterwards we can use them like any other types.

1abstract class EntityStoreClass<T> {
2 abstract insert(entity: T): void
3 abstract get(): T;
4}
5
6interface EntityStoreInterface<T> {
7 insert: (entity: T) => void;
8 get: () => T;
9}
10
11type EntityStoreType<T> = {
12 insert: (entity: T) => void;
13 get: () => T;
14}

Do you have to use T?

The short answer is no. You can name type variables what you want. The long answer: it’s a convention that spans multiple languages, and it makes sense in a lot of cases: T is short for “type” and using a single letter is a subtle signal that you're looking at a type variable.

That said, choose whatever makes sense for your codebase.

Can I make my generics more specific?

Imagine we have a function that stores data to a database and returns the result. We could write something like:

1const update = async <T>(entity: T) => {
2 const storedEntity = await db().collection('myThings').insert(entity);
3 return storedEntity;
4}

But what if our function only works on objects which have an id property? This function would break as we could pass it anything.

To restrict what types a type variable can be assigned to, we can use the extends keyword. A type is said to extend a given type if it could be assigned to that type.

1// Define the type that our entity has to extend
2type Entity = { id: string };
3
4// Only allow our entity (`T`) to be a type that extends `Entity`
5// V
6const update = <T extends Entity>(entity: T) => {
7 const storedEntity = await db().collection('myThings').insert(entity);
8 return storedEntity;
9}
10
11update('users', { id: '56760d39', name: 'Me' }); // Works - returns a User
12update('users', number) // TypeScript will show an error

Now our function will preserve the type of the entity we pass it. But it will only let us store things that fit the type Entity.

Conclusion

Generics are an incredibly powerful feature of TypeScript. They allow us to write reusable code without sacrificing type safety. Better yet, once you get to know them, they're an easy feature to work with for the power they bring.

© 2020 by Jonathan Wilkinson. All rights reserved.
Theme by LekoArts