skip to content

In TypeScript, given `class Store<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } }`, when is `T` decided, and what changes if the type parameter is moved onto `add` instead of onto the class?

level: middleimportance: should knowfreq 42%

answer

  1. where the parameter lives decides its lifetime
  2. fixed once per instance, not per call
  3. the field and the method must agree
  4. one call's arguments only? put it on the method

basics

~20 s

A class type parameter is decided once, when the class is instantiated or annotated, and stays fixed for that instance type, so the field and the method share it. Moving it onto the method makes it fresh at every call, breaking that link to the stored items.

solid answer

~50 s

`T` is bound per instantiation: `new Store<string>()` — or inference from a constructor argument — fixes it once, and from then on `items` is `string[]` and `add` accepts only `string`. Every member of that instance sees the same `T`, which is what makes the field and the method agree. If you instead write `add<U>(item: U)`, `U` is chosen fresh at each call, so one call could pass a `string` and the next a `number` with nothing tying either to `items` — the field would have to become `unknown[]` and you would have lost the guarantee you wanted. The rule of thumb: put the parameter on the class when it relates several members, such as state plus the methods that touch it; put it on the method when it relates only that one call's arguments and return type.

code

typescript · 20 lines
typescript
class Store<T> {
  private items: T[] = [];
  add(item: T): void { this.items.push(item); }
  all(): readonly T[] { return this.items; }
}

const strings = new Store<string>();
strings.add("a");
const read = strings.all(); // readonly string[]

// Parameter on the method instead: chosen per call, related to nothing stored
class Wrapper {
  wrap<U>(value: U): { value: U } {
    return { value };
  }
}

const w = new Wrapper();
const a = w.wrap("a"); // { value: string }
const b = w.wrap(1);   // { value: number }

go deeper

for a junior

Know that new Store<string>() fixes the element type for that object, so add takes only strings and reading back gives strings — the choice is made once, not per call.

for a middle

Explain the scope difference: a class parameter is bound per instantiation and shared by every member, while a method parameter is bound per call, and show why the field's element type collapses if you move it down.

for a senior

Apply the placement rule when designing an API: keep the parameter where the relationship lives, and avoid parameterizing a class for something only one method needs, which forces every construction site to decide a type it does not care about.

for a principal

Own the surface cost. Each class type parameter is a decision you impose on every construction site forever, so weigh the invariant it enforces against the friction it adds, and prefer method-level parameters when the relation is local to one call.

## Two different scopes A type parameter is always attached to *something*, and the thing it is attached to determines when it is chosen and how long it lasts. - A parameter on the **class** is chosen when the class is instantiated (or when you write the type), and is fixed for the whole lifetime of that instance type. - A parameter on a **method** is chosen at each call, and lasts for that one call. Everything else follows from that. ## Class-level: fixed at instantiation ```typescript class Store<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } all(): readonly T[] { return this.items; } } const s = new Store<string>(); s.add("a"); // ok // s.add(1); // error: number is not assignable to string s.all(); // readonly string[] ``` One decision — `string` — propagates to the private field, the method parameter and the method return type. This is the point of a class-level parameter: it *relates members to one another*. `add` and `all` are not independently typed; they are locked to the same element type because they mention the same parameter. The argument can also be inferred rather than written. If the constructor takes something mentioning `T`, `new Store(["a"])` infers `Store<string>`; when nothing constrains it, the checker falls back to `unknown`, which is a good hint that the parameter should have been supplied explicitly. ## Method-level: fresh per call ```typescript class Formatter { wrap<U>(value: U): { value: U } { return { value }; } } const f = new Formatter(); f.wrap("a"); // { value: string } f.wrap(1); // { value: number } ``` Here the two calls choose different types from the same object, which is correct — nothing about the *object* is element-typed. The parameter relates only the argument to the return type of that call. ## Why moving it breaks the relation Now imagine moving `Store`'s parameter down: ```typescript class BadStore { private items: unknown[] = []; add<U>(item: U): void { this.items.push(item); } } ``` `U` is bound per call and has nowhere to go: the field cannot be `U[]` because there is no single `U` for the object, so it degrades to `unknown[]`. Read-back is now untyped, and every consumer has to narrow. The class-level parameter was carrying the invariant "everything in here is the same type", and moving it deleted that invariant. This is the concrete reason "put the parameter where the relationship lives" is not a style preference. The converse mistake also exists: parameterizing the *class* for something only one method uses forces every construction site to decide a type it does not care about, and often collapses to `Foo<unknown>` in practice. ## Where a class parameter is visible Within the declaration, `T` is in scope for: ```typescript class Store<T> extends Base<T> implements Repository<T> { private items: T[] = []; constructor(initial: T[]) { super(); this.items = [...initial]; } add(item: T): void { this.items.push(item); } } ``` — field types and initializers, the constructor, instance methods, and the heritage clause, where it can be forwarded to a base class or an implemented interface. It is not in scope on the static side, which belongs to the single shared constructor object. ## Two instantiations are two types `Store<string>` and `Store<number>` are distinct types produced from one declaration, and one is not assignable to the other. That is the checker preserving the very relation the parameter was declared to express. There is still only one `Store` at runtime; the distinction is entirely in the type layer and disappears on compilation. ## What interviewers listen for That you answer "when is `T` decided" precisely — once per instantiation, not per call, not at runtime — and that you can articulate the placement rule in terms of *what relates to what* rather than as a recipe. The strongest answers show the failure mode explicitly: move the parameter down and the field's element type has nothing to be, so the class stops guaranteeing anything about its own contents.

  • If the constructor takes no argument mentioning T and you write `new Store()`, what is T?
    With nothing to infer from, the checker falls back to `unknown`, so `items` is `unknown[]` and `add` accepts anything. That silent fallback is usually a bug signal: either supply the argument explicitly as `new Store<string>()`, or give the constructor a parameter that lets inference do the work.
  • Are `Store<string>` and `Store<number>` assignable to each other?
    No — they are distinct types produced from the same declaration, and after substitution their members have unrelated types. That is the checker preserving the relation the parameter expresses. At runtime there is still one `Store` constructor; the distinction lives entirely in the type layer.
  • Where inside the class declaration is the class type parameter usable?
    Field types and initializers, the constructor, instance methods, and the heritage clause — so you can forward it as `extends Base<T>` or `implements Repository<T>`. The one place it is not usable is the static side, because statics belong to the single constructor object shared by every instantiation.

saying these in an interview costs you the question

  • Thinks T is chosen per method call on a generic class
  • Says T is looked up at runtime from stored values
  • Puts the parameter on the class when one method uses it
  • Assumes Store<string> is assignable to Store<number>
  • Believes new Store() errors rather than falling back to unknown

context