skip to content

Why does TypeScript reject a static member that references the class type parameter, as in `class Box<T> { static empty: T }`, and what do you write instead when you want a static factory for a generic class?

level: middleimportance: should knowfreq 40%

answer

  1. instance side versus static side
  2. one constructor object for every instantiation
  3. which T would the static hold?
  4. give the static its own parameter

basics

~20 s

TypeScript reports that static members cannot reference class type parameters. Statics live on the single constructor object shared by every instantiation, so there is no one T for them. Give the static its own type parameter instead.

solid answer

~40 s

A class type parameter belongs to the *instance* type: `Box<string>` and `Box<number>` are two types, but there is only one `Box` constructor object at runtime and only one static slot on it. A static member typed `T` would have to be `string` and `number` simultaneously, so the compiler rejects it with "Static members cannot reference class type parameters". The fix is to give the static its own parameter, which is fixed per call rather than per instance: `static of<U>(value: U): Box<U> { return new Box(value); }`. That is the standard generic-factory shape, and `Box.of("hi")` infers `Box<string>`. Other options are a standalone factory function, or a static whose type is a *concrete* instantiation such as `Box<string>` — mentioning `Box<string>` in a static is fine, mentioning the bare parameter is not.

code

typescript · 22 lines
typescript
class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  // A static member may NOT reference T:
  //   static empty: T;
  // error: Static members cannot reference class type parameters.

  // It may declare its own type parameter, bound per call:
  static of<U>(value: U): Box<U> {
    return new Box(value);
  }

  // A concrete instantiation is fine on the static side:
  static readonly EMPTY_TEXT: Box<string> = new Box("");
}

const a = Box.of("hi");   // Box<string>
const b = Box.of(42);     // Box<number>

go deeper

for a junior

Recognise the error message "Static members cannot reference class type parameters" and know that the class's parameter is usable in fields, the constructor and instance methods but not on statics.

for a middle

Explain the sharing argument — one constructor object serves every instantiation, so a static slot cannot hold a per-instantiation type — and write the static of<U> factory that fixes it.

for a senior

Decide where the member really belongs: an instance method, a static with its own parameter, or a plain module-level factory, and justify the choice by what the API's callers actually need to write.

for a principal

Own the consequence for library surface: statics are a shared, unparameterized namespace, so factories, registries and caches on the static side need explicit parameters or keys rather than leaning on a type the runtime never sees.

## The error ```typescript class Box<T> { // static empty: T; // error: Static members cannot reference class type parameters. // static make(v: T): Box<T> // same error } ``` The message is literal and worth memorizing: *static members cannot reference class type parameters*. It applies to static fields, static method signatures and static accessors alike. ## Why the rule exists A class declaration produces two things: an **instance type** (`Box<T>`, what `new Box(...)` gives you) and a **static side** (the constructor object `Box` itself, which carries the statics). The type parameter is part of the instance type — it is chosen when you instantiate. The static side is not parameterized: there is exactly one `Box` object in the program, shared by every instantiation. So ask what `static empty: T` would even mean. Code elsewhere writes `Box<string>` and `Box<number>`; both refer to the same runtime `Box` and therefore the same `Box.empty` slot. There is no per-instantiation copy of a static to hold a per-instantiation type. Rather than pretend otherwise and produce an unsound type, the checker refuses the declaration. This is a real difference from languages where generics are reified: in C#, `Box<string>` and `Box<int>` genuinely have separate static storage, so a static field typed `T` is meaningful there. TypeScript's type layer is erased, so it is not. ## What is in scope where The class type parameter *is* in scope for: - instance property types and their initializers, - constructor parameter and body types, - instance method parameter and return types, - the heritage clause: `class Store<T> extends Base<T> implements Repository<T> {}`. It is **not** in scope for anything on the static side. ## The fixes **1. Give the static its own type parameter.** A static method may declare parameters of its own; they are bound per call, which is exactly what a factory wants: ```typescript class Box<T> { value: T; constructor(value: T) { this.value = value; } static of<U>(value: U): Box<U> { return new Box(value); } } const b = Box.of("hi"); // Box<string> ``` Naming it `U` rather than `T` is not cosmetic — it emphasises that this is a *different*, independently chosen parameter that happens to feed the same class. **2. Use a standalone factory function.** Nothing forces the factory to be a static at all: ```typescript function box<U>(value: U): Box<U> { return new Box(value); } ``` This is often the cleaner answer in a module-based codebase, and it sidesteps the static side entirely. **3. Type the static with a concrete instantiation.** The rule bans the bare parameter, not the class: ```typescript class Box<T> { value: T; constructor(value: T) { this.value = value; } static readonly EMPTY_STRING_BOX: Box<string> = new Box(""); } ``` That compiles, because `Box<string>` is a finished type with nothing unresolved in it. **4. Move the member to the instance side.** If the thing genuinely depends on `T`, it usually belongs on instances, where `T` is bound. A `clone(): Box<T>` instance method is unproblematic. ## The related trap: `new` inside a generic Candidates often reach for statics because they want "a default value of type `T`". There is no such thing — the checker cannot conjure a value for an arbitrary type parameter, and there is no runtime token for `T` to construct from. If you need to build a `T`, the caller must supply either a value or a factory (`() => T`), because the type argument has been erased by the time the code runs. ## What interviewers listen for That you separate the instance type from the static side and explain the sharing argument in one sentence, rather than reciting the error text. The strongest answers add the erasure point — one constructor object exists at runtime, so per-instantiation statics are not representable — and then produce the `static of<U>` fix without hesitation, because that shape appears in real container and builder APIs.

  • Can a static method have its own type parameter even though it cannot use the class's?
    Yes, and that is the standard fix. `static of<U>(value: U): Box<U>` declares `U` on the method itself, so it is chosen fresh at each call site rather than per instantiation. `Box.of("hi")` infers `Box<string>` and `Box.of(42)` infers `Box<number>` from the same static.
  • Is a static typed `Box<string>` allowed inside `class Box<T>`?
    Yes. The restriction is on referencing the unresolved parameter `T`, not on mentioning the class. `Box<string>` is a finished type with nothing left to substitute, so `static readonly EMPTY: Box<string> = new Box("")` compiles fine.
  • Why can't a generic class just construct a default value of type T when it needs one?
    Because `T` has no runtime existence — there is no token to call `new` on and no way to ask what type was supplied. If the class needs to produce a `T`, the caller has to hand it in, either as a value or as a factory function typed `() => T`.

There is only one Box blueprint in the filing cabinet, no matter how many differently-typed boxes you build from it. A static slot is written on the blueprint itself, so it would have to say "string" and "number" at the same time.

saying these in an interview costs you the question

  • Says statics can use T if you add a constraint
  • Thinks each instantiation gets its own copy of the statics
  • Confuses the ban with static methods being non-generic entirely
  • Claims the class parameter is out of scope in instance methods too
  • Expects the runtime to supply a default value for T

context