Default Type Parameters
Just as function parameters can have default values, generic type parameters can have default types. Writing <T = string> means callers who omit the type argument get string, while those who need something else can still supply it. Defaults make generic APIs friendlier — common cases require no annotation — without sacrificing flexibility. They also interact with constraints and with parameter ordering in specific ways worth understanding. This page covers the syntax, how defaults combine with constraints, ordering rules, and the difference between a default and a constraint.
Declaring a default
Assign a default with = after the parameter name (and after its constraint, if any). When the caller does not provide the type argument and the compiler cannot infer one, the default is used.
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const a: ApiResponse = { data: "anything", status: 200 };
// a.data: unknown — used the default
const b: ApiResponse<string[]> = { data: ["x"], status: 200 };
// b.data: string[] — explicit argument wins
Without a default, ApiResponse would force every use to specify a type argument. With T = unknown, the bare ApiResponse is valid and data falls back to unknown — a safer default than any.
Defaults with constraints
A parameter can have both a constraint and a default. The constraint limits what callers may pass; the default supplies the type when they pass nothing. The default must itself satisfy the constraint.
interface Collection<T extends { id: number } = { id: number }> {
items: T[];
findById(id: number): T | undefined;
}
// ❌ Error: default `string` does not satisfy `extends { id: number }`
// interface Bad<T extends { id: number } = string> { items: T[] }
The order is fixed: T extends Constraint = Default. The default { id: number } is assignable to the constraint { id: number }, so it is legal. A default that violated the constraint would be a compile error.
Defaults are not inferred away
A default applies only when no type argument is given and none can be inferred. If the compiler can infer a parameter from the arguments, inference wins over the default.
function createState<T = number>(initial: T): { value: T } {
return { value: initial };
}
const s1 = createState(0); // T inferred as number → { value: number }
const s2 = createState("hi"); // T inferred as string → { value: string }
const s3 = createState<boolean>(true); // explicit → { value: boolean }
Here the default = number rarely triggers because T is always inferable from initial. Defaults matter most when a parameter appears only in positions the compiler cannot infer from, such as a return type or a phantom field.
Ordering: defaulted parameters come last
Like optional function parameters, type parameters with defaults must follow those without. A required parameter cannot appear after a defaulted one, because callers supply type arguments positionally.
// ✅ required first, defaulted last
function request<TBody, TResult = TBody>(body: TBody): TResult {
return body as unknown as TResult;
}
// ❌ Error: required type parameter cannot follow a default
// function bad<T = string, U>(a: T, b: U): void {}
A default may reference an earlier parameter, as TResult = TBody does — a handy way to say “the result defaults to the same type as the input.” This only works because TBody is declared first.
| Feature | Constraint (extends) | Default (=) |
|---|---|---|
| Purpose | Restrict allowed types | Supply a fallback type |
| Applies when | Always | Only when omitted and not inferred |
| Position | T extends C | T = D (after any constraint) |
| Can reference earlier params | Yes | Yes |
A default is not a constraint.
<T = string>lets callers pass any type, defaulting tostringonly when omitted. To restrict what may be passed, add a constraint:<T extends string = string>. The two are often used together.
Best Practices
- Add a default when most callers want one common type but flexibility is still useful.
- Prefer
= unknownover= anyas a safe fallback for unconstrained data. - Ensure each default satisfies its own constraint:
T extends C = DrequiresDassignable toC. - Place defaulted type parameters after all required ones, mirroring optional function parameters.
- Let a later default reference an earlier parameter (
TResult = TBody) for “same as input” behavior. - Remember inference still overrides the default whenever the compiler can resolve the type.