Skip to content
Theory · Generics · lesson 9 of 10

Generic classes

Classes can have a type parameter too. Picture a stack of containers on the quay. The crane puts a new container on top and takes off the top one as well. The stack does not care what is inside the containers, but one stack holds one kind of cargo: a shared sign on it reads "bananas only".

In programs such a pile is called a stack: the last element put on is the first taken off. A stack of numbers and a stack of strings work the same way; only the element type differs. So the stack class needs a type parameter.

A class with a type parameter

A generic class declares a type parameter after the class name, and that parameter is visible in all its fields and methods except static ones:

TypeScript
class Stack<T> {
    private items: T[] = [];
    push(item: T): void {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
}

const words = new Stack<string>();
words.push("a");
words.push("b");
console.log(words.pop(), words.pop(), words.pop()); // b a undefined

Let's take the class apart:

  • class Stack<T> declares the type parameter T for the whole class;
  • items: T[] is a private array holding elements of type T;
  • push(item: T) takes only values of type T;
  • pop(): T | undefined returns the top element, or undefined if the stack is empty.

Now let's see what happens when it runs:

  1. new Stack<string>() creates a stack and puts string in place of T. For this object push takes a string, and pop returns string | undefined.
  2. Two push calls put "a", then "b".
  3. The first pop takes off "b", the second "a", and the third finds the stack empty and returns undefined. The program prints b a undefined.

That is why the type of pop's result includes undefined. TypeScript does not count your push calls and does not know whether the stack is empty right now.

The type argument sets the sign on the stack. If you put a number into words, tsc does not let it through: words.push(5) is an error.

TEXT
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

A classic interview task is to write a typed stack or queue: class Stack<T> with push and pop methods. The heart of the answer is one type parameter for the whole class and T | undefined on pop.

Map, Set and Promise

Many built-in JavaScript classes are generic in TypeScript too:

TypeScript
const ages = new Map<string, number>();
ages.set("ann", 30);
const age = ages.get("ann");
console.log(age); // 30

const tags = new Set(["ts", "js"]);
console.log(tags.has("ts")); // true

Let's see what happens here:

  1. Map<K, V> takes two type parameters: K for keys and V for values. In new Map<string, number>() the keys are strings and the values are numbers.
  2. get is declared as get(key: K): V | undefined, so age has the type number | undefined: the key may be missing.
  3. Set<T> holds values of one type. In new Set(["ts", "js"]) no type argument is written, and TypeScript infers T from the constructor's argument, as for a function: the result is Set<string>.

Promise<T> works the same way: a promise that eventually gives a value of type T. For example, Promise<string> promises a string.

Here the stack picture ends. You can take the sign off a real stack and hang another. An object's type is set when it is created and never changes: words stays a Stack<string> for good.

Common mistake

An object of a generic class is created without a type argument:

TypeScript
class Stack<T> {
    private items: T[] = [];
    push(item: T): void {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
}

const words = new Stack();
words.push("a");
const last = words.pop();
console.log(last.toUpperCase()); // ✗
TEXT
error TS18046: 'last' is of type 'unknown'.

new Stack() has neither a type argument nor constructor arguments, and words has no annotation. So T becomes unknown, as for a function with nothing to infer a type from. You can put anything into such a stack, a string or a number, but you cannot use an element you took off without checking it. Write the type argument: new Stack<string>().

Key points

  • A generic class declares a type parameter after its name: class Stack<T>, and T is visible in all fields and methods except static ones.
  • The type argument is given when the object is created: in new Stack<string>() the push method takes only strings, and pop returns string | undefined.
  • Map<K, V>, Set<T> and Promise<T> are generic classes too; in new Set(["ts", "js"]) T is inferred from the constructor's argument, as for a function.
  • With no type argument, no constructor arguments and no annotation on the variable, T becomes unknown.