skip to content

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