What are Generics in Typescript

avatar

Borislav Hadzhiev

Last updated: Feb 21, 2021

banner

Photo from Unsplash

Generics in typescript allow us to pass types as a variable to functions and classes.

Generic classes example #

Let's look at an oversimplified example, say we want to have an array that can store elements of different types, let's start with strings and numbers:

class NumberArray { constructor(public arr: number[]) {} insert(el: number): void { this.arr.push(el); } print(): void { console.log(this.arr); } } class StringArray { constructor(public arr: string[]) {} insert(el: string): void { this.arr.push(el); } print(): void { console.log(this.arr); } }

Looking at the above example we have a lot of code duplication, the only differences in the implementation of our Number and String array classes are the types associated with the elements stored in the arrays.

Ideally we would want to provide a more generic implementation, by providing a variable for the type of elements our arrays store at initialization time - that's where generics help us:

class GenericArray<T> { constructor(public arr: T[]) {} insert(el: T): void { this.arr.push(el); } print(): void { console.log(this.arr); } } const stringArr = new GenericArray<string>(['hello', 'world']); stringArr.insert('!!!'); stringArr.print(); const numberArr = new GenericArray<number>([1, 2, 3]); numberArr.insert(4); numberArr.print();

We have defined a GenericArray class that takes a type T as a generic argument. Upon initialization of the class we can provide a variable type in the < > syntax to specify with what type the T in our class should be replaced with.

In our case we have explicitly specified the generic when initializing the class, however typescript can also infer it, similar to how it infers the return types of functions.

- const stringArr = new GenericArray<string>(['hello', 'world']) + const stringArr = new GenericArray(['hello', 'world'])

The way typescript is able to infer that T is going to be of type string in this case is because in our constructor function we have specified that the argument we pass in is of type T[], therefore typescript can look at the types of elements that make up the array passed to the constructor function and infer the type/types from there.

Best practice is to be explicit about the generic type, because it helps us catch errors.

Generic Functions example #

The syntax/concept around using generics with functions is the same:

function returnInArray<T>(value: T): T[] { return [value]; } const stringArray = returnInArray<string>('hello world'); const numberArray = returnInArray<number>(123);

Generic Constraints #

We can also apply constraints around generic to only allow certain types to be passed as generics.

class Shark { swim() { console.log('The shark swims'); } } class Dolphin { swim() { console.log('The dolphin swims'); } } interface CanSwim { swim(): void; } function callSwim<T extends CanSwim>(obj: T): void { obj.swim(); } callSwim<Dolphin>(new Dolphin()); callSwim<Shark>(new Shark());

In the example we have a function called callSwim which takes in an object parameter and calls the swim function on it. Because not all objects have a swim function we have to use a constraint with our generic type, to only allow for objects that confirm to the CanSwim interface, in other words implement the swim function.

Summary #

Generics allow us to make our functions/classes more generic by passing in types as call time parameters. They are very similar to function parameters but instead of values we pass in types.

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.