• Class instance members
  • Class methods
  • function arguments
  • function return value

Consider the simple (first in, first out) data structure implementation. A simple one in TypeScript / JavaScript looks like:

One issue with this implementation is that it allows people to add anything to the queue and when they pop it - it can be anything. This is shown below, where someone can push a string onto the queue while the usage actually assumes that only numbers where pushed in:

  1. class Queue {
  2. private data = [];
  3. push = (item) => this.data.push(item);
  4. pop = () => this.data.shift();
  5. }
  6. const queue = new Queue();
  7. queue.push(0);
  8. queue.push("1"); // Oops a mistake
  9. // a developer walks into a bar
  10. console.log(queue.pop().toPrecision(1));
  11. console.log(queue.pop().toPrecision(1)); // RUNTIME ERROR

One solution (and in fact the only one in languages that don’t support generics) is to go ahead and create special classes just for these contraints. E.g. a quick and dirty number queue:

  1. class QueueNumber {
  2. private data = [];
  3. push = (item: number) => this.data.push(item);
  4. pop = (): number => this.data.shift();
  5. }
  6. queue.push(0);
  7. queue.push("1"); // ERROR : cannot push a string. Only numbers allowed
  8. // ^ if that error is fixed the rest would be fine too

Of course this can quickly become painful e.g. if you want a string queue you have to go through all that effort again. What you really want is a way to say that whatever the type is of the stuff getting pushed it should be the same for whatever gets popped. This is done easily with a generic parameter (in this case, at the class level):

  1. /** A class definition with a generic parameter */
  2. class Queue<T> {
  3. private data = [];
  4. pop = (): T => this.data.shift();
  5. }
  6. /** Again sample usage */
  7. const queue = new Queue<number>();
  8. queue.push(0);
  9. queue.push("1"); // ERROR : cannot push a string. Only numbers allowed
  10. // ^ if that error is fixed the rest would be fine too

In this section you have seen examples of generics being defined at class level and at function level. One minor addition worth mentioning is that you can have generics created just for a member function. As a toy example consider the following where we move the reverse function into a Utility class:

  1. class Utility {
  2. reverse<T>(items: T[]): T[] {
  3. var toreturn = [];
  4. for (let i = items.length - 1; i >= 0; i--) {
  5. toreturn.push(items[i]);
  6. }
  7. return toreturn;
  8. }
  9. }

I’ve seen people use generics just for the heck of it. The question to ask is what constraint are you trying to describe. If you can’t answer it easily you might have a useless generic. E.g. the following function

    Here the generic T is completely useless as it is only used in an single argument position. It might as well be:

    1. declare function foo(arg: any): void;

    In this case you can see that the type is only used in one place. So there is no constraint between members. You would be equivalent to a type assertion in terms of type safety:

    1. declare function parse(name: string): any;
    2. const something = parse('something') as TypeOfSomething;

    Generics used only once are no better than an assertion in terms of type safety. That said they do provide convenience to your API.

    A more obvious example is a function that loads a json response. It returns a promise of whatever type you pass in:

    1. const getJSON = <T>(config: {
    2. url: string,
    3. headers?: { [key: string]: string },
    4. }): Promise<T> => {
    5. const fetchConfig = ({
    6. method: 'GET',
    7. 'Accept': 'application/json',
    8. 'Content-Type': 'application/json',
    9. ...(config.headers || {})
    10. });
    11. return fetch(config.url, fetchConfig)
    12. .then<T>(response => response.json());
    13. }

    Note that you still have to explicitly annotate what you want, but the getJSON<T> signature (config) => Promise<T> saves you a few key strokes (you don’t need to annotate the return type of loadUsers as it can be inferred):

    1. type LoadUsersResponse = {
    2. users: {
    3. name: string;
    4. email: string;
    5. }[];
    6. }
    7. function loadUsers() {
    8. return getJSON<LoadUsersResponse>({ url: 'https://example.com/users' });
    9. }