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:
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 undefinedLet's take the class apart:
class Stack<T>declares the type parameterTfor the whole class;items: T[]is a private array holding elements of typeT;push(item: T)takes only values of typeT;pop(): T | undefinedreturns the top element, orundefinedif the stack is empty.
Now let's see what happens when it runs:
new Stack<string>()creates a stack and putsstringin place ofT. For this objectpushtakes a string, andpopreturnsstring | undefined.- Two
pushcalls put"a", then"b". - The first
poptakes off"b", the second"a", and the third finds the stack empty and returnsundefined. The program printsb 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.
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:
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")); // trueLet's see what happens here:
Map<K, V>takes two type parameters:Kfor keys andVfor values. Innew Map<string, number>()the keys are strings and the values are numbers.getis declared asget(key: K): V | undefined, soagehas the typenumber | undefined: the key may be missing.Set<T>holds values of one type. Innew Set(["ts", "js"])no type argument is written, and TypeScript infersTfrom the constructor's argument, as for a function: the result isSet<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:
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()); // ✗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>, andTis visible in all fields and methods except static ones. - The type argument is given when the object is created: in
new Stack<string>()thepushmethod takes only strings, andpopreturnsstring | undefined. Map<K, V>,Set<T>andPromise<T>are generic classes too; innew Set(["ts", "js"])Tis inferred from the constructor's argument, as for a function.- With no type argument, no constructor arguments and no annotation on the variable,
Tbecomesunknown.