Generic parameter defaults

Give a type parameter a fallback type so callers can omit it and still get a precise type.

Signature
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
  element?: T,
  children?: U
): Container<T, U>;
  • Added in TypeScript 2.3

Parameters

ParameterTypeDescription
$Toptionaltype parameter with constraint and default= HTMLDivElementA type parameter is deemed optional when it has a default. The default must satisfy the parameter's constraint, if it has one.
$Uoptionaltype parameter with default= T[]A default may refer to type parameters declared before it, so `U = T[]` is legal. Required type parameters must not follow optional ones.

Errors

Nothing at runtime. tsc reports error 2706, "Required type parameters may not follow optional type parameters", and error 2344 when the default type does not satisfy the constraint.

Changelog

  • PHP 2.3Introduced. A declaration that merges with an existing class or interface may add a default to an existing type parameter, or introduce a new type parameter provided it specifies a default.

Writing = SomeType after a type parameter makes it optional. Callers may omit it, and the parameter resolves to the default instead. The handbook's rules are short and worth knowing exactly:

  • A type parameter is optional if it has a default.
  • Required type parameters must not follow optional ones — the ordering rule people trip over.
  • The default must satisfy the parameter's constraint, if there is one.
  • When supplying type arguments you only supply the required ones; the rest resolve to their defaults.
  • If a default is specified and inference cannot choose a candidate, the default is inferred rather than unknown.

When to reach for it

Defaults collapse overload sets. The handbook's example replaces three create declarations with one. They also give an ergonomic entry point to a type that has grown extra parameters: adding E = Error to Result<T> breaks no existing reference.

What trips people up

A default is not a constraint. <T = string> still accepts T = number when the caller says so, or when inference finds a candidate — the default only applies when nothing else determines the parameter. Combine extends and = when you want both a bound and a fallback.

Example

Example · typescript
interface Result<T, E = Error> {
  ok: boolean;
  value?: T;
  error?: E;
}

// Only the required parameter is supplied; E resolves to Error
declare function request<TBody, TQuery = Record<string, string>>(
  path: string,
  query?: TQuery
): Promise<Result<TBody>>;

// Illegal — error 2706, a required parameter follows an optional one:
// interface Bad<T = string, U> {}

const failed: Result<number> = { ok: false, error: new Error("timeout") };
console.log(failed.ok, failed.error?.message);

When to use it

  • Add an error type to Result<T> without touching existing Result<T> references
  • Replace an overload set whose only difference is an omitted type argument
  • Default a state container's action type to a catch-all union
  • Give a React-style props interface a default element type

Frequently asked questions

Why do I get "Required type parameters may not follow optional type parameters"?

Because once a type parameter has a default, every parameter after it must have one too — TypeScript matches type arguments positionally, so there is no way to skip a defaulted parameter and supply a later required one. Reorder the list so required parameters come first, or give the trailing parameters defaults.

Does a default type argument restrict what callers can pass?

No. A default only says what the parameter resolves to when nothing else determines it; callers may still supply any type, and inference may still pick a different one. Add extends alongside the default when you want a genuine bound: <T extends string = "a">.

When does the default actually get used?

When the caller omits the type argument and inference finds no candidate for that parameter. If the parameter appears in an argument position and a call supplies a matching value, inference wins and the default is ignored.

More examples

Collapsing an overload set

The single defaulted declaration covers all three call shapes, and U = T[] shows that a default may refer to an earlier type parameter. The comments record what each call resolves to — this is a declaration file sketch, so the values themselves are never constructed.

Example · typescript
interface Container<T, U> {
  element: T;
  children: U;
}

// Before: three declarations differing only in what was omitted
declare function createOld(): Container<string, string[]>;
declare function createOld<T>(element: T): Container<T, T[]>;
declare function createOld<T, U>(element: T, children: U[]): Container<T, U[]>;

// After: one declaration with defaults
declare function create<T = string, U = T[]>(
  element?: T,
  children?: U
): Container<T, U>;

const a = create();                 // Container<string, string[]>
const b = create(42);               // Container<number, number[]>
const c = create(42, ["x", "y"]);   // Container<number, string[]>
console.log(a, b, c);

Default plus constraint

extends Action bounds what A may ever be, while = { type: string } says what it is when nobody chooses. Output would be "loose anything" then "strict inc"; the difference is at compile time, where strict.dispatch({ type: "anything" }) is rejected and loose.dispatch is not.

Example · typescript
interface Action {
  type: string;
}

interface Store<S, A extends Action = { type: string }> {
  state: S;
  dispatch(action: A): void;
}

type Counter = { count: number };
type CounterAction = { type: "inc" } | { type: "dec" };

// A resolves to the default { type: string }
const loose: Store<Counter> = {
  state: { count: 0 },
  dispatch: (action) => console.log("loose", action.type),
};

// A is supplied, so only "inc" | "dec" is accepted
const strict: Store<Counter, CounterAction> = {
  state: { count: 0 },
  dispatch: (action) => console.log("strict", action.type),
};

loose.dispatch({ type: "anything" });
strict.dispatch({ type: "inc" });

Discussion

  • Be the first to comment on this lesson.