Generics in typescript

May 08, 2020

If you worked with typescript you may have heard about generics, which may be used in types like array or promises. But, did you ever understand what, why, and when using them?

Let me give you a small overview. First point: keep in mind you always use types for your objects and don’t use the any type to make your code flexible. With these 2 points in mind let’s start with Generics.

Generics basically allows you to create a reusable code not linked with a single type.

I will do an example with code: I need to create a box to save my photos, also a helper manager to add, get, and remove.

My solution:

  • Create the class Photo with information about the entity photo.
  • Create the class Manager. It contains an array of photos and actions for add, remove, and delete.
  • Class Box with photos property. It works as the manager.
class Photo {
   readonly id = 1;
   constructor(public name: string){   }
}
class Manager {
   items: Photo[] = []
   add(item: Photo) {
   	this.items.push(item);
   }
   remove(item: Photo){
   	this.items = this.items.filter( i => i.id === item.id);
   }
   get(id: number): Photo | undefined {
   	return this.items.find(i => i.id === id)
   }
}
class Box {
   photos: Manager = new Manager();
}

let box = new Box();
box.photos.add(new Photo("hello.jpg"));
console.log(box.photos.items);

It works, but tomorrow we’d like to allow adding videos. The first solution would be to duplicate the manager class for the new class video. The second solution would be using any type into the manager class, but that’s a bit dangerous because the manager will store any type of object without restriction and this is not what we want.

Maybe using an interface is a solution, because the manager is linked to the interface and if the video fits with the interface I can store any class, but the class needs to implement the interface…so let me try.

interface Item {
   readonly id: number;
}
class Photo implements Item{
   readonly id = 1;
   constructor(public name: string){   }
}

class Video implements Item {
   readonly id = 1;
   quality: string = "4K"
}

class Manager {
   items: Item[] = []
   add(item: Item) {
   	this.items.push(item);
   }
   remove(item: Item){
   	this.items = this.items.filter( i => i.id === item.id);
   }
   get(id: number): Item | undefined {
   	return this.items.find(i => i.id === id)
   }
}
class Box {
   photos: Manager = new Manager();
   videos: Manager = new Manager();
}

let box = new Box();
box.photos.add(new Photo("hello.jpg"));
box.videos.add(new Video());
box.videos.add(new Photo("fuck.mp4"));
console.log(box.photos.items);
console.log(box.videos.items);

It works, and forces every object to be an Item, but the photos can save videos, and this is not my plan.Here’s where Generics come to help us.

Using Generics we set the type for the manager using the keyword, force the type specified when the manager is declared and functions, and classes are related to the type in runtime.

interface Item {
   readonly id: number;
}
class Photo implements Item{
   readonly id = 1;
   constructor(public name: string){
   }
}
class Video implements Item {
   readonly id = 1;
   quality: string = "4K"
}
class Manager<T extends Item> {
   items: T[] = []
   add(item: T) {
   	this.items.push(item);
   }
   remove(item: T){
   	this.items = this.items.filter( i => i.id === item.id);
   }
   get(id: number): T | undefined {
   	return this.items.find(i => i.id === id)
   }
}
class Box {
   photos: Manager<Photo> = new Manager<Photo>();
   videos: Manager<Video> = new Manager<Video>();
}
let box = new Box();
box.photos.add(new Photo("hello.jpg"));
box.videos.add(new Video());
box.videos.add(new Photo("fuck.mp4"));
//compiler and IDE raise a error notify the photo cannot be stored into the video
console.log(box.photos.items)
console.log(box.videos.items)

If we try to add a photo into the video, the IDE and compiler will show an error because you are trying to add a photo into the video array.

As you can see Generics is a powerful way to write flexible and reusable code with type rules.

When can we use Generics? When we detect some behaviors that are repeated by different types and can be used in functions or classes.

Hopefully, that will give you a bit of help with Generics in Typescript. If you enjoyed this post, don’t forget to share it!