Generic classes

Parameterise a class so each instance carries its own element type across fields, methods and returns.

Signature
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

Parameters

ParameterTypeDescription
$NumTypetype parameterA type variable declared after the class name. It is in scope for instance fields, methods, accessors and the constructor, and is bound when the class is instantiated — by inference from constructor arguments or explicitly, as in `new GenericNumber<number>()`.

Errors

Nothing at runtime from the generics themselves. tsc reports error 2302, "Static members cannot reference class type parameters", when a static field or method mentions the class type parameter.

Changelog

  • PHP 2.3Class type parameters may carry defaults, so `class Box<T = string> {}` can be instantiated as `new Box()`.
  • PHP 4.2`abstract new () => T` construct signatures let a generic factory accept abstract classes as well as concrete ones.
  • PHP 5.0The `const` modifier is permitted on class type parameters, but not on those of interfaces or type aliases.

A class type parameter is written after the class name and is in scope for the instance side: fields, methods, accessors and the constructor. It is bound at construction, either inferred from constructor arguments or written explicitly as new Stack<string>().

The static-side rule

Quoting the handbook: generic classes are generic only over their instance side, so static members cannot use the class's type parameter. This falls out of how classes work at runtime — there is one constructor object shared by every instantiation, so there is no single T for a static member to mean. Static caches, counters and factory methods must therefore either be non-generic or declare their own type parameter:

static of<U>(value: U): Box<U> {
  return new Box(value);
}

When to reach for it

Use a generic class when state and behaviour travel together: collections, caches, result wrappers, builders, observable stores. If the type only shows up in one method, make that method generic instead and leave the class plain.

What trips people up

Two instantiations of the same generic class are structurally compared by their members, so Box<string> and Box<number> are unrelated types but instanceof Box is true for both — narrowing with instanceof cannot recover the element type.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Build a typed Stack, Queue or LinkedList whose pop returns the element type
  • Wrap an in-memory cache so get and set agree on the value type
  • Model a Result or Option container with map and unwrap methods
  • Write a static factory that infers the element type from its argument

Frequently asked questions

Why can't a static member use the class type parameter?

Because generic classes are generic only over their instance side — one constructor object is shared by Box<string> and Box<number>, so a static member has no single T to mean. TypeScript reports error 2302; the fix is to give the static method its own type parameter, as in static of<U>(value: U): Box<U>.

Do I have to write new Stack<string>() or will TypeScript infer it?

TypeScript infers class type arguments from the constructor's parameters, so new Box(42) gives Box<number>. Write the argument explicitly when the constructor takes nothing that mentions T — such as an empty collection — otherwise the parameter falls back to unknown or its default.

Is Box<string> assignable to Box<number>?

No, in normal code — the member types differ, so the structural check fails. The exception is a parameter position where the compared members are method-shorthand declarations, which TypeScript checks bivariantly; declaring members as properties with function types keeps the check strict.

More examples

A static factory with its own type parameter

Box.of infers U from its argument, so boxed is Box<number> without any angle brackets at the call site, and each map produces a new Box with the callback's return type. Output is "21 42 value=42". The commented-out static field is the error the handbook warns about: a static member has no per-instantiation T to refer to.

Example · typescript
class Box<T> {
  constructor(readonly value: T) {}

  // Illegal: static empty: T;  -> error 2302
  // Legal: the static method declares its own U
  static of<U>(value: U): Box<U> {
    return new Box(value);
  }

  map<U>(fn: (value: T) => U): Box<U> {
    return new Box(fn(this.value));
  }
}

const boxed = Box.of(21);
const doubled = boxed.map((n) => n * 2);
const described = doubled.map((n) => `value=${n}`);

console.log(boxed.value, doubled.value, described.value);

A constrained typed cache

The extends Entity constraint is what lets set read entity.id inside the class body; without it the property would be unknown on T. The keys are sorted before logging so the output is deterministic: [ 'a', 'b' ] 12.

Example · typescript
interface Entity {
  id: string;
}

class EntityCache<T extends Entity> {
  private store = new Map<string, T>();

  set(entity: T): void {
    this.store.set(entity.id, entity);
  }

  get(id: string): T | undefined {
    return this.store.get(id);
  }

  get ids(): string[] {
    return [...this.store.keys()].sort();
  }
}

interface Product extends Entity {
  price: number;
}

const cache = new EntityCache<Product>();
cache.set({ id: "b", price: 30 });
cache.set({ id: "a", price: 12 });

console.log(cache.ids, cache.get("a")?.price);

Discussion

  • Be the first to comment on this lesson.