Generic Classes & Interfaces
Generic interfaces and type aliases
Section titled “Generic interfaces and type aliases”An interface or type alias can take type parameters exactly like a function. The classic is a typed container:
interface Box<T> { value: T;}
const numBox: Box<number> = { value: 42 };const strBox: Box<string> = { value: "hi" };numBox.value.toFixed(2); // ✅ value is numberType aliases work the same way and are often better for unions and functions:
type Pair<A, B> = { first: A; second: B };type Result<T> = { ok: true; value: T } | { ok: false; error: string };
const r: Result<number> = { ok: true, value: 10 };Result<T> is a generic discriminated union — one of the most useful patterns in TypeScript, and something we return to in the error-handling lesson.
Generic classes: the parameter is fixed per instance
Section titled “Generic classes: the parameter is fixed per instance”When a class is generic, its type parameter is chosen when you construct an instance, and every method of that instance shares it:
class Stack<T> { private items: T[] = [];
push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; }}
const numbers = new Stack<number>();numbers.push(1);numbers.push("two"); // ❌ Argument of type 'string' is not assignable to 'number'const top = numbers.pop();// ^? number | undefinedOnce you write new Stack<number>(), T is number for the life of that object. Often you don’t even write it — new Stack() with a first push(1) can’t infer, but many designs let inference fill T from a constructor argument.
Per-method type parameters
Section titled “Per-method type parameters”A type parameter declared on a method (not the class) is chosen fresh on each call, independent of the class parameter:
class Collection<T> { constructor(private items: T[]) {}
// T is the class parameter (fixed per instance) first(): T | undefined { return this.items[0]; }
// U is a method parameter (fresh per call) map<U>(fn: (item: T) => U): U[] { return this.items.map(fn); }}
const c = new Collection([1, 2, 3]); // T = numberconst lengths = c.map((n) => n.toString().length);// ^? number[] — U inferred as number, per this callconst labels = c.map((n) => `item-${n}`);// ^? string[] — U inferred as string, a different callT is decided once (at construction); U is decided every time you call map. Knowing which parameter belongs to the class and which to the method is the key to reading generic class signatures.
Implementing a generic interface
Section titled “Implementing a generic interface”A class can implement a generic interface either by fixing the parameter or by staying generic itself:
interface Repository<T> { getById(id: string): T | undefined; save(entity: T): void;}
// fix the parameter to a concrete typeclass UserRepository implements Repository<User> { getById(id: string): User | undefined { /* ... */ return undefined; } save(entity: User): void { /* ... */ }}