What are Generics in Typescript

avatar

Borislav Hadzhiev

Sun Feb 21 20212 min read

banner

Photo by Tolga Ulkan

Generics are used when your functions and classes take variables of different types

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 doplhin 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.

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee