skip to content

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