skip to content

Generic Interfaces, Classes & Type Aliases

Parameterizing a shape instead of a call: Box<T>, Repository<T>, or a Result<T, E> alias reused across a codebase. Interviewers ask this to see whether you model containers and API envelopes with one type or with copy-pasted variants.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

Given the TypeScript declaration `interface ApiResponse<T> { data: T; status: number }`, what does the `<T>` declare, and what is the type of `res.data` when `res` is annotated `ApiResponse<User[]>`?

level: juniorimportance: must knowfreq 72%

answer

  1. placeholder filled in at the use site
  2. one declaration, many payload types
  3. the argument substitutes for every T
  4. ApiResponse<User[]> makes data User[]

basics

~10 s

The <T> declares a type parameter: a placeholder the user of the interface fills in. Writing ApiResponse<User[]> substitutes User[] for T everywhere in the declaration, so res.data has type User[] while status stays number.

solid answer

~40 s

`<T>` on the declaration makes `ApiResponse` a *type constructor* rather than a finished type: it takes one type argument and produces a concrete shape. `T` is a placeholder scoped to that declaration, usable in any member position inside it. At the use site, `ApiResponse<User[]>` substitutes `User[]` for every occurrence of `T`, so `res.data` is `User[]`, `res.data[0]` is `User`, and `res.status` is still `number`. The payoff is that one declaration replaces `UserResponse`, `OrderResponse` and friends, and the relationship between the envelope and its payload is expressed once. None of this exists after compilation — a generic interface emits no JavaScript at all, so the type argument is a compile-time check on the shape you claim to have, not a runtime validation of the JSON you actually received.

go deeper

for a junior

Be able to read a parameterized declaration and say what each member's type becomes for a given type argument, and state that the parameter is a placeholder the caller fills in.

for a middle

Explain that the checker substitutes the argument for every occurrence of the parameter, that each instantiation is a distinct type, and that the parameter is scoped to its own declaration.

for a senior

Show why parameterizing the varying slot beats duplicated envelopes in a real API layer, and be explicit that the annotation asserts a shape rather than verifying the data that actually arrived.

for a principal

Own the API-surface consequence: a published generic envelope fixes a contract every consumer instantiates, so adding a parameter or changing the slot's meaning is a breaking change across every downstream instantiation.

## The problem a generic declaration solves Without type parameters, a shape that wraps *some other type* has to be re-declared per payload: ```typescript interface UserResponse { data: User[]; status: number } interface OrderResponse { data: Order[]; status: number } ``` Every envelope field is now written twice, and adding `requestId` means editing every copy. A generic declaration parameterizes the varying slot instead of duplicating the fixed ones. ## Declaring the parameter ```typescript interface ApiResponse<T> { data: T; status: number; } ``` `<T>` after the name introduces a **type parameter**. It is an identifier that stands for a type not yet known, in exactly the way a function parameter stands for a value not yet known — which is why the analogy "generics are functions at the type level" is a fair one. The name is arbitrary; `T` is only convention, and `interface ApiResponse<Payload>` is equally valid and often clearer. ## What happens at the use site `ApiResponse` on its own is not a type you can annotate with — it is incomplete. You supply a **type argument** to finish it: ```typescript declare const res: ApiResponse<User[]>; res.data; // User[] res.data[0]; // User res.status; // number ``` The checker substitutes `User[]` for `T` throughout the body of the declaration. `ApiResponse<User[]>` and `ApiResponse<Order[]>` are two different types produced from one declaration; assigning one to a variable of the other is an error, because after substitution their `data` members have unrelated types. ## Where the parameter is in scope `T` is visible anywhere inside the declaration it belongs to — property types, method parameters, method return types, and nested positions: ```typescript interface Box<T> { value: T; items: T[]; map(fn: (v: T) => T): Box<T>; } ``` Note the last line: the declaration may mention itself with the same parameter, which is how container types describe operations that preserve their element type. `T` is *not* visible outside the declaration — there is no global `T` afterwards. ## The same mechanism on classes and aliases The parameter list is a property of the declaration, not of interfaces specifically. A class carries one the same way, and its members see it: ```typescript class Store<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } } const s = new Store<string>(); ``` A type alias does too, and because an alias can name any type — not just an object shape — a parameterized alias can describe a union, a tuple or a function type: ```typescript type Result<T, E> = | { ok: true; value: T } | { ok: false; error: E }; ``` `Result<User, Error>` is that union with the substitutions applied. ## Nothing survives compilation An interface emits no JavaScript whatsoever, and neither do type arguments. `ApiResponse<User[]>` compiles to nothing; the emitted code just reads `res.data`. This has one consequence a candidate is expected to state out loud: annotating a value as `ApiResponse<User[]>` asserts a shape, it does not verify one. If the server returns `{ data: null }`, the type says `User[]` and the program crashes at the first `.length`. Validating an unknown payload — parsing it and narrowing — is a separate job the type layer does not do for you. ## Common confusions - **"T must be called T."** No — any identifier works, and descriptive names read better in public APIs. - **"The generic checks the data."** No — substitution happens in the checker; the runtime sees plain property access. - **"`ApiResponse<any>` is the same as `ApiResponse<User[]>`."** They are different types; the first has switched off checking of `data` entirely, which is usually the thing you were trying to avoid. ## What interviewers listen for That you can read the substitution mechanically ("`T` becomes `User[]`, so `data` is `User[]`"), that you name the parameter as belonging to the declaration rather than to any one use, and that you volunteer the erasure point without being pushed to it. Candidates who describe generics as "a way to accept any type" have described `any`; the point is that the *relationship* between `data` and the type argument is preserved.

  • Can the type parameter appear in a method signature inside the interface, not just on a property?
    Yes — it is in scope everywhere inside that declaration. `interface Box<T> { value: T; map(fn: (v: T) => T): Box<T> }` uses it as a property type, a callback parameter type and part of the return type, and all three refer to the same substituted type for a given `Box<string>`.
  • Does declaring a generic interface add anything to the emitted JavaScript?
    Nothing at all. Interfaces and type arguments are erased, so the output contains only the value code that used them. That is why annotating a fetch result as `ApiResponse<User[]>` proves nothing about the bytes that arrived — if you need certainty you have to parse and check the payload yourself.
  • Are `ApiResponse<User>` and `ApiResponse<Order>` assignable to each other?
    No. Each instantiation is a distinct type once the substitution is done, and their `data` members have unrelated types, so the checker rejects the assignment in both directions. That is the whole point of parameterizing the slot rather than typing it as `any`.

saying these in an interview costs you the question

  • Thinks the type parameter must literally be named T
  • Says the generic validates the payload at runtime
  • Believes a generic interface emits a JavaScript object
  • Claims ApiResponse<any> and ApiResponse<User[]> are equivalent
  • Describes generics as just a way to accept any type

context

open as a page

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%

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.

open as a page

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%

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.

open as a page

In a TypeScript API layer you find UserResponse, OrderResponse and InvoiceResponse, each repeating `status` and `requestId` and differing only in the type of `data`. How would you consolidate them into one declaration, and when is a single generic declaration the wrong answer?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Parameterize the varying slot: declare one ApiResponse<T> with status, requestId and data of type T, then use ApiResponse<User>, ApiResponse<Order> and so on. It is the wrong model when the variants differ in which fields exist, not just in one field's type.

open as a page