skip to content

Declaring Generics

The syntax surface: where a type-parameter list can go — functions, interfaces, classes, type aliases — and how those parameters are declared, defaulted, and combined. This is the warm-up half of a generics interview, before anyone asks you to make inference behave.

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

questions

12

In TypeScript, what does the `= unknown` in a declaration like `interface ApiResult<T = unknown>` do, and when does the compiler actually apply that default?

level: juniorimportance: must knowfreq 55%

answer

  1. what happens when you omit the argument
  2. affects references, not only calls
  3. inference runs first
  4. default only when no candidate exists
  5. explicit arguments turn inference off

basics

~20 s

A type-parameter default supplies the type argument when the caller omits it, so writing ApiResult with no argument means ApiResult<unknown>. TypeScript also falls back to the default in a generic call when inference finds no candidate for that parameter.

solid answer

~40 s

`= unknown` makes `T` an *optional* type parameter with a default. At a reference site you may then write `ApiResult` on its own and it means `ApiResult<unknown>`; without the default, `ApiResult` alone would be an arity error because the compiler expects one type argument. In a generic *function*, the default is a last resort rather than a preference: TypeScript first tries to infer the parameter from the arguments, and only when inference produces no candidate at all does it fall back to the default. A default never overrides a successful inference. And if you write the type arguments explicitly, inference is switched off completely, so any omitted trailing parameters take their defaults instead of being inferred. Like the rest of the type layer, the default is erased and leaves nothing in the emitted JavaScript.

code

typescript · 13 lines
typescript
interface ApiResult<T = unknown> {
  status: number;
  data: T;
}

// No type argument: T falls back to the default.
const raw: ApiResult = { status: 200, data: JSON.parse("{}") };
const typed: ApiResult<{ id: string }> = { status: 200, data: { id: "u1" } };

declare function box<T = string>(value?: T): T[];

const a = box();   // string[] - no inference candidate, default applies
const b = box(42); // number[] - inference wins, default ignored

go deeper

for a junior

Know the syntax and say plainly that a default lets a reference omit the type argument, so ApiResult means ApiResult<unknown>. Be ready to write one on an interface.

for a middle

Explain the ordering: inference collects candidates first and a default is used only when there are none, so a successful inference always wins. Mention that a default also changes how many type arguments a reference may legally supply.

for a senior

Show judgment in the choice of default type on a shared API: unknown keeps callers honest, any silently disables checking, and a concrete default optimises the common case at the cost of being wrong when it is wrong. Note that adding a default is a non-breaking change while removing one is not.

for a principal

Own defaults as an evolution lever across a published type surface: they are how a generic gains parameters without breaking every existing reference, and how you keep the ergonomic short form alive. Argue for a house rule on what defaults are permitted, since a wide default becomes the de facto contract nobody ever revisits.

## What a type-parameter default is A generic declaration lists type parameters between angle brackets: `interface ApiResult<T> { status: number; data: T }`. Every reference to that type must then supply a type argument — `ApiResult<User>`. Writing `= unknown` after the parameter name turns it into an *optional* type parameter with a default type argument: ```ts interface ApiResult<T = unknown> { status: number; data: T; } ``` Now `ApiResult` and `ApiResult<unknown>` mean exactly the same thing. The syntax mirrors a default value on a function parameter, but it lives entirely in the type layer: it is a default *type*, chosen by the compiler while checking, not a value assigned at runtime. ## Reference sites: the arity you are allowed to write For a generic type, the default changes the legal number of type arguments. With `interface Envelope<Payload, Meta = string>`, a reference may supply one or two arguments — `Envelope<User>` or `Envelope<User, Date>` — and supplying zero is still an error because `Payload` has no default. The compiler reports this as a range: a generic type with one required and one defaulted parameter requires between one and two type arguments. There is no placeholder syntax: you cannot skip `Payload` in order to specify `Meta`. Defaults are therefore only useful on *trailing* parameters, which is also why the language requires every parameter after the first defaulted one to have a default of its own. ## Generic functions: inference first, default second For a generic function the default behaves differently from what most people expect. TypeScript's first move at a call site is inference: it matches the argument types against the parameter types and collects candidates for each type parameter. The default is consulted only for a parameter that ended up with **no** candidate at all. ```ts declare function box<T = string>(value?: T): T[]; const a = box(); // T has no inference candidate -> default applies -> string[] const b = box(42); // T inferred as number -> default ignored -> number[] ``` So a default is a floor, not a preference. Without it, `box()` would produce `unknown[]`, since a parameter with no candidates falls back to `unknown` (historically `{}`). Adding `= string` simply changes what that empty case resolves to. This is the single most common misunderstanding on this topic: candidates always win, so a default cannot be used to "nudge" inference toward a wider or narrower type. ## Explicit type arguments switch inference off If you write type arguments at the call site, the compiler does not infer anything — it uses your list and fills the missing trailing entries from their defaults: ```ts declare function pair<K, V = boolean>(k: K, v: V): [K, V]; pair("a", 1); // both inferred -> [string, number] pair<string>("a", 1); // V is NOT inferred; V = boolean, so 1 is an error ``` That surprises people who reach for a default hoping it will let them pin one parameter and infer the rest. It does not: partial type-argument inference does not exist, and a default only supplies a fixed type for the entries you left off. ## Choosing the default The choice of default type carries real weight in a shared API: - `unknown` is the safe neutral default. Callers who never supply an argument still have to narrow before using the value, so nothing is silently unchecked. - `any` is convenient and dangerous: every value flowing through that position stops being checked, and users who forget to pass an argument lose type safety without any diagnostic. - A concrete default such as `Error` or `string` is the most ergonomic when one type genuinely dominates real usage — it makes the common reference short — but it is a claim about the data, and it is a lie whenever the caller's real type differs and they forget to say so. - `never` occasionally appears as a deliberately hostile default that forces callers to supply the argument, though simply leaving the parameter required is clearer. ## Erasure None of this exists after compilation. The default is resolved by the checker and then thrown away along with the rest of the annotations; the emitted JavaScript contains no record that `T` existed, let alone what it defaulted to. A type-parameter default and a default function parameter value look alike and are unrelated: the latter emits an assignment, the former emits nothing at all.

  • If a generic function's type parameter has a default, does the default win over a type inferred from the arguments?
    No. Inference runs first and any successful candidate wins; the default applies only when the parameter collected no candidate at all. With `declare function box<T = string>(value?: T): T[]`, `box(42)` gives `number[]`, not `string[]`. A default raises the floor for the empty case; it never overrides inference.
  • Why is `unknown` usually a better default than `any` for a public generic?
    Both let callers omit the argument, but `any` disables checking for every value that flows through that position, so a caller who forgets to specify the type silently loses safety with no diagnostic. `unknown` keeps the value opaque and forces a narrowing step before use, which turns the omission into a visible, local decision rather than a hole.
  • Does adding a default to an existing type parameter break existing code?
    No. Adding a default only widens what is accepted: every reference that already passed the argument still compiles unchanged, and references that omitted it were errors before. Removing a default is the breaking direction, as is adding a new parameter without one.

saying these in an interview costs you the question

  • Says the default overrides a type inferred from the arguments
  • Thinks omitting the type argument yields any rather than the default
  • Confuses a type-parameter default with a default function parameter value
  • Claims the default emits a runtime fallback in the JavaScript
  • Believes a default lets you specify only some type arguments at a call site

context

open as a page

In TypeScript, what does declaring `function identity<T>(x: T): T` give you that `function identity(x: any): any` does not?

level: juniorimportance: must knowfreq 84%

basics

~20 s

A type parameter links the argument type to the return type, so the call site keeps the concrete type it passed in and stays type-checked. any turns checking off instead: the result is unchecked and any misuse of it silently compiles.

open as a page

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%

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.

open as a page

In TypeScript, what rules govern default type parameters — where a default may appear in the list, what type it may reference, and how it interacts with an `extends` constraint on the same parameter?

level: middleimportance: should knowfreq 38%

basics

~20 s

Defaults must come last: once a type parameter has one, every later parameter needs one too. A default may reference parameters declared before it but not after, and when the parameter is also constrained the default must itself satisfy that constraint.

open as a page

In TypeScript, given `declare function pair<K, V>(k: K, v: V): [K, V]`, can you call `pair<string>('a', 1)` to pin K and let V be inferred? What is the rule for how many type arguments a call may supply?

level: middleimportance: should knowfreq 48%

basics

~20 s

No. TypeScript has no partial type-argument inference: a call supplies every type parameter that has no default, or none at all. Omitted trailing parameters take their declared defaults rather than being inferred from the arguments.

open as a page

In TypeScript, what is the difference between `type Mapper = <T>(x: T) => T[]` and `type MapperOf<T> = (x: T) => T[]`?

level: middleimportance: should knowfreq 41%

basics

~20 s

Position decides who picks T. In the first, the type takes no arguments and describes a generic function whose T is chosen fresh at each call. In the second, the type itself takes an argument and describes an ordinary non-generic function.

open as a page

In a `.tsx` file, `const identity = <T>(x: T) => x;` fails to compile even though the identical line is fine in a `.ts` file. Why, and how do you write a generic arrow function there?

level: middleimportance: should knowfreq 52%

basics

~20 s

In .tsx files the parser reads <T> as the opening tag of a JSX element, not a type parameter list, so the line never parses as a generic arrow. Disambiguate with a trailing comma, <T,>, with a constraint, or by using a function declaration.

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

You are designing a public TypeScript type whose declaration is shaped like `Query<TData, TError, TKey>`. How do you decide the order of the type parameters and which of them get defaults, and what does that decision commit you to later?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Order type parameters by how often callers will specify them, most-specified first, because a caller cannot skip an earlier one to reach a later one. Default the tail so the common reference stays short, and remember that only a new defaulted parameter appended at the end is non-breaking.

open as a page

When modelling a TypeScript function whose result type depends on its argument, when would you reach for a type parameter rather than writing a set of overload signatures?

level: seniorimportance: should knowfreq 37%

basics

~20 s

Use a type parameter when one uniform rule relates argument to result across an open set of types, and overloads only when a small fixed set of unrelated argument shapes produce unrelated results. Overloads also break on union arguments and on unlisted types.

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