Understanding typescript generics by example

Typescript generics are a built in language feature using the same principles of generics in other languages like C#, Java, etc.

To understand generics, let's talk about functions.

Functions take parameters...

const getUser = async (id: number) => {
  const resp = await fetch(`/api/users/${id}`);
  const user = await resp.json();
  return user;
}

In this case, the id is a function parameter.

Well, in some cases, you might want to also pass a type as a parameter too, that's when you can use a generic

One super useful time to use a generic is when creating an SDK for an API.

interface User {
  id: number;
  name: string;
}

interface Repository<RepoType> {
  getById(id: number): RepoType;
}

class UserRepository implements Repository<User> {
    async getById(id: number) {
      const resp = await fetch(`/api/users/${id}`);
      const user = await resp.json();
      return user;
    }
}

const repo = new UserRepository();
const user = repo.getById(1);

In the above example, we're creating an interface with a generic parameter for the RepoType. This will allow us to create other types of repositories for different data models, but leveraging the code re-usability of the underlying fetch logic.

A similar thing can be done with just pure functions instead of classes.

async function fetchAPI<DataType>(url: string, opts?: FetchOptions) {
    const resp = await fetch(url);
    const json = await resp.json();

    return json as DataType;
}

const fetchUserById = async (id: number) => {
    const user = await fetchAPI<User>(`/api/users/${id}`);
    return user;
}

const user = fetchUserById(1);

A lot of examples around the internet are fairly contrived, and also tend to use single letter generic names like <T>, <P, S>, etc. While the short name is more terse, it can also make things a little hard to read.

The following is a quick and dirty implementation of the queue data structure. Don't worry too much about that, a queue is basically like waiting in line for a ride at Disney World. You enter the line, first, you're the first person to get on the ride. (First in first out or FIFO).

function createQueue<Type> () {
    const queue: Type[] = [];

    return {
        add(obj: Type) {
            queue.push(obj);
        },
        remove() {
            return queue.shift();
        },
        peek() {
            return queue[0];
        }
    }
}

interface Task {
    name: string;
    work: () => Promise<void>;
}

const taskQueue = createQueue<Task>();
taskQueue.add({ name: 'thing', work: () => {}});
taskQueue.add({ name: 'another', work: () => {}});
taskQueue.add({ name: 'last thing', work: () => {}});

const task = taskQueue.remove();
assert.equal(task.name, 'thing'); // True!

So, in this case we're able to create a strongly typed queue called taskQueue when we remove an item from the queue using taskQueue.remove(), the task will be a Task. This means your IDE will be able to properly understand what the task is as well, which is massivley helpful.

In the above examples, we're manually passing the generic parameter, and you can think of the generic in terms of using "of". For example, the queue above, is a Queue "of" Task types, and the UserRepostory is a Repository "of" type User.

It's also possible to infer the generic parameter from the actual function parameters.

const find = function<ItemType>(
  items: ItemType[],
  fn: (t: ItemType) => boolean
): ItemType | null {
    for (const item of items) {
        if (fn(item)) {
            return item;
        }
    }

    return null;
}

interface Avenger {
  name: string;
}

const avengers: Avenger[] = [
  { name: 'iron man' },
  { name: 'hulk' },
  { name: 'thor' }
 ];

const strongestAvenger = find(
  avengers,
  (avenger) => avenger.name === 'thor',
 );

Here we have a find function. Notice that the ItemType is the generic parameter name. When we call the find function, the ItemType parameter will automatically get passed the Avenger type because we said in the function parameters that the items are of ItemType. You can use ItemType | null as the return type too.