Generic Classes
A generic class carries one or more type parameters that are bound when you create an instance, letting a single class definition serve many concrete types. Containers, caches, queues, and result wrappers are all natural fits — write the logic once and let the type parameter flow through fields, constructor arguments, and methods. This page covers declaring generic classes, constructor inference, constraints on class type parameters, the important rule that static members cannot use them, and generic methods that add their own parameters.
Declaring a generic class
The type parameter goes in angle brackets after the class name and is then visible across instance fields and methods. A type-safe Box that holds a single value is the canonical example.
class Box<T> {
constructor(private value: T) {}
get(): T {
return this.value;
}
set(value: T): void {
this.value = value;
}
}
const numberBox = new Box(123); // Box<number> — inferred from the argument
numberBox.set(456);
// numberBox.set("nope"); // ❌ Error: 'string' is not assignable to 'number'
const stringBox = new Box<string>("hi"); // explicit type argument
TypeScript infers T from the constructor argument, so new Box(123) produces a Box<number>. Every method then enforces that type, which is why set("nope") is rejected.
A reusable data structure
Generic classes really pay off for collections, where the element type must stay consistent across many methods. Here is a small typed queue.
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
get size(): number {
return this.items.length;
}
}
const tasks = new Queue<string>();
tasks.enqueue("build");
tasks.enqueue("test");
const next = tasks.dequeue(); // string | undefined
One Queue<T> definition gives you a fully typed Queue<string>, Queue<number>, or Queue<User> with zero duplication.
Constraints on class type parameters
Class type parameters accept extends constraints just like functions. Constrain T when the class needs to rely on certain properties or methods of its elements — for instance, a repository that requires every entity to have an id.
interface Entity {
id: number;
}
class Repository<T extends Entity> {
private store = new Map<number, T>();
save(entity: T): void {
this.store.set(entity.id, entity); // safe: T always has `id`
}
findById(id: number): T | undefined {
return this.store.get(id);
}
}
const users = new Repository<User>(); // User must have a numeric `id`
Without the constraint, entity.id would be a compile error because an arbitrary T is not known to have an id property.
Static members cannot use the class type parameter
A class type parameter is bound per instance, so static members — which belong to the class itself — cannot reference it. If a static helper needs to be generic, give it its own method-level type parameter.
class Container<T> {
constructor(public value: T) {}
// ❌ Error: Static members cannot reference class type parameters.
// static defaultValue: T;
// ✅ Static method with its own type parameter
static of<U>(value: U): Container<U> {
return new Container(value);
}
}
const c = Container.of("hello"); // Container<string>
If you find yourself wanting a static member typed by
T, it almost always belongs on the instance, or it needs an independent type parameter on the static method itself.
Generic methods and inheritance
A method can introduce its own type parameter in addition to the class’s, and a subclass can either fix the parent’s parameter or stay generic by passing it through.
class Collection<T> {
protected items: T[] = [];
add(item: T): void {
this.items.push(item);
}
// method-level parameter `U`, independent of class `T`
mapTo<U>(fn: (item: T) => U): U[] {
return this.items.map(fn);
}
}
// Subclass fixes T = string
class StringCollection extends Collection<string> {
join(sep = ", "): string {
return this.items.join(sep);
}
}
// Subclass stays generic, forwarding T
class LoggedCollection<T> extends Collection<T> {
add(item: T): void {
console.log("adding", item);
super.add(item);
}
}
Class vs interface vs function generics
| Use case | Best tool |
|---|---|
| Stateful container with behavior | Generic class |
| Pure shape contract, no implementation | Generic interface |
| One-off transformation, no state | Generic function |
| Shared logic across subtypes | Generic class + constraint |
Best Practices
- Let the constructor infer the type argument when possible; annotate only when it cannot.
- Add
extendsconstraints when the class relies on specific members of its elements. - Never try to type a
staticmember with the class type parameter; use a static method’s own parameter. - Give methods their own type parameters when their type is independent of the instance’s.
- Decide deliberately whether a subclass fixes the parent’s parameter or forwards it.
- Keep fields
private/protectedand expose typed methods to preserve the generic invariants. - Prefer a generic interface when you only need a contract with no shared implementation.