TypeScript Generics

By the end of this article you should know:

  1. What generics are in TypeScript and how to write them
  2. When generics are used and why

Introduction: What Are Generics?

Recently, I started work on a new project using React Native which has given me the chance to start learning TypeScript. After a week or so of playing with the language I started to come across the term "generics", which I was unfamiliar with, when I searched around for a simple explanation on the web, I found answers like this:

Generics are a tool that allow developers to parametrise types.

Hmmmmm

If you're familiar with other typed languages, such as Java or C#, the above might not seem that opaque to you. However, I have to admit that at first I found this concept confusing. I was unfamiliar with typed languages, let alone what it might mean to "parametrise types". Additionally, having built applications in vanilla Javascript and React.js in the past, I couldn't initially see what the motivation for any of this was.

However, after some exploration I've come to see that just as functions are a way to reuse code on different values, generics are a way to reuse code in a program, whist using a typed language.

Spells and Potions In An RPG

The best explanation I've found online, which makes clear what generics are useful for, comes from this talk by Tyler Leonhardt (https://www.youtube.com/watch?v=3aewd6qcm6A) where he describes a game he built "for wizards", where players can have separate inventories for their spells and potions.

In his talk, he makes his points purely with TypeScript code, but if you're new to the language, it's useful to start with JavaScript and see why generics could be introduced. The Tyler-inspired-JS-code to be discussed is as follows:

class ItemCollection {
    constructor(private isConsumable) {}
    private items = []
    pickUp(item) {
        this.items.push(item)
    }
}

class Mage extends Magic.Mage {
    spells = new ItemCollection(false)
    potions = new ItemCollection(true)
}

What would stop a player adding the wrong type of item to an ItemCollection? The current answer is Nothing. We don't have anything in place which would prevent our players from being able to add Potions to their Spell collections and vice versa.

const MyMage = new Mage();
MyMage.potions.pickUp(Magic.FireSpell);

//This is not something we want to allow, but could possibly happen.

JS Solution 1

What solutions to this problem would we have in JavaScript? One solution might be to add logic to our items, so they have to be defined with an isConsumable Boolean, and then we could extend our pickUp(item) function so that it sorts the items accordingly.

But this isn't ideal. What if we want to add a third inventory later, such as weapon, which would also be uncomsumable. This solution is undesirably brittle as we would still be able to add the new objects to our spells inventory.

JS Solution 2

Another solution we could try is to build a separate inventory class for each item type, and give each new item a string to denote its "type".

Smiley

This way our feature is extendable. We can add more inventorys to our characters, if we want. But every time we do, we are going to copy and paste a fair bit of code.

export class PotionCollection {
    constructor(private isConsumable) {}
    private items = []
    pickUp(item) {
        if item.type == 'Potion'{
                this.items.push(item)
        }
    }
}

export class SpellCollection {
    constructor(private isConsumable) {}
    private items = []
    pickUp(item) {
        if item.type == 'Spell'{
                this.items.push(item)
        }
    }
}

export class WeaponCollection {
    constructor(private isConsumable) {}
    private items = []
    pickUp(item) {
        if item.type == 'Weapon'{
                this.items.push(item)
        }
    }
}

Could there be feature in TypeScript which could help make this code (much) more DRY and reusable?

Generics Are The Solution To This Problem

A Quick Anatomy Lesson

As the discussion so far probably suggests, generics are the solution to our problem. But what are they? Consider the following code:

function RepeatMe<T>(arg: T): T {
  return arg;
}

Type Variables

In angle brackets, we have added a "type variable" <T> . As you might have guessed, we use "T" to stand for type by convention, but any letter would have worked (<U> would have been fine, for instance).These variables give us the ability to provide type information to the function, and dictate its return type. This allows us to make the function "generic", so that it works using a variety of different types- whilst avoiding the drawbacks associated with our untyped JS code.

What if a function has multiple arguments, which all could have different types? Type variables allow us to handle this situation too. As we are able to add as many Type variables as we would like. As below

function EchoMe<T, U>(arg: T, arg2: U): T {
  return [arg, arg2];
}

To call our function, we now have to pass in what types are being given to the function when we call it. If we pass in the wrong type, the TypeScript compiler will notice and complain.

RepeatMe<Number>(22);
//22
RepeatMe<Number>("hello");
//index.ts:5:18 - error TS2345: Argument of type '"hello"' is not assignable to parameter of type 'Number'.

This appears to be the solution we were looking for. Luckily for us, generics work for Classes the very same way.

Classes and Interfaces

By adding generics to our ItemCollection class from earlier, we solve our problem of keeping our code nice and DRY while avoiding problems with our many inventories. Given the code above, what the code below achieves should be easy to infer:

export class ItemCollection<T> {
  constructor(private isConsumable: boolean) {}
  private items: T[] = [];
  pickUp(item: T) {
    this.items.push(item);
  }

  use(callback: (item: T) => void) {
    const item = Magic.getRandomItem(this.items, this.isConsumable);

    if (item) {
      callback(item);
    }
  }
}

class Mage extends Magic.Mage {
  spells = new ItemCollection<Magic.ISpell>(false);
  potions = new ItemCollection<Magic.IPotion>(true);
}

//Now when you try to add a spell to potions inventory and vice versa it won't let you

Side note: something to be aware of is that classes only use generics on their "static" side, when you are writing code pertaining to the whole class, rather than its instances.

Conclusion

After reading this article you should:

  1. What problem generics are addressing
  2. How some problems which are solved through generics in TS are approached in JS
  3. How to write and use generics in your own typescript code

Good Luck and thanks for reading