<T = Fallback>) allow you to make generic type parameters optional by providing intelligent fallback defaults.1. The Problem with Required Generic Parameters#
Consider an API Response interface defined with two generic type parameters (TData and TError):
// Both TData and TError are required type parameters
interface ApiResponse<TData, TError> {
status: number;
message: string;
data: TData | null;
error: TError | null;
}Every time a developer uses ApiResponse, they MUST explicitly specify both type parameters—even for standard endpoints where errors are simple Error objects:
// ❌ VERBOSE: Forced to specify 'Error' every single time!
const res1: ApiResponse<{ id: string }, Error> = {
status: 200,
message: "OK",
data: { id: "101" },
error: null,
};
// ❌ Compiler Error: Generic type 'ApiResponse<TData, TError>' requires 2 type argument(s).
// const res2: ApiResponse<{ id: string }> = { ... };
2. Defining Default Generic Types (<T = Default>)#
By assigning a fallback type using the = syntax, you make a generic type parameter optional. If the consumer omits the type argument, TypeScript automatically uses the fallback type:
// 🟢 TError defaults to 'Error', TData defaults to 'unknown'
interface ApiResponse<TData = unknown, TError = Error> {
status: number;
message: string;
data: TData | null;
error: TError | null;
}
// 1. Omit BOTH type parameters (TData = unknown, TError = Error):
const defaultRes: ApiResponse = {
status: 500,
message: "Internal Error",
data: null,
error: new Error("Database offline"),
};
// 2. Specify ONLY TData (TError automatically defaults to 'Error'):
const userRes: ApiResponse<{ name: string }> = {
status: 200,
message: "OK",
data: { name: "Alice" },
error: null,
};
// 3. Override BOTH TData and TError explicitly:
const customErrorRes: ApiResponse<{ name: string }, { code: number; detail: string }> = {
status: 400,
message: "Validation Failed",
data: null,
error: { code: 1044, detail: "Email invalid" },
};3. Combining Constraints with Defaults (<T extends Bound = Default>)#
You can combine Generic Constraints (extends) with Default Arguments (=) in a single declaration:
The syntax order is: <T extends Constraint = DefaultType>
// 1. Constraint: TElement MUST be an HTMLElement (or subclass)
// 2. Default: Defaults to HTMLDivElement if omitted
interface UIComponent<TElement extends HTMLElement = HTMLDivElement> {
element: TElement;
mount(container: HTMLElement): void;
}
// 🟢 Defaults to UIComponent<HTMLDivElement>
const defaultDivComponent: UIComponent = {
element: document.createElement("div"),
mount(parent) {
parent.appendChild(this.element);
},
};
// 🟢 Explicitly specified as UIComponent<HTMLButtonElement>
const buttonComponent: UIComponent<HTMLButtonElement> = {
element: document.createElement("button"),
mount(parent) {
parent.appendChild(this.element);
},
};
// ❌ Compiler Error: Type 'string' does not satisfy the constraint 'HTMLElement'.
// const invalidComponent: UIComponent<string>;
4. Parameter Ordering Rules#
Just like standard JavaScript function default parameters (function fn(a, b = 10)), required generic parameters MUST precede optional generic parameters:
// 🟢 VALID ORDER: Required parameter 'K' comes before optional parameter 'V'
type ValidMap<K extends string, V = unknown> = Record<K, V>;
// ❌ INVALID ORDER: Compiler Error! Required type parameters may not follow optional type parameters.
/*
type InvalidMap<V = unknown, K extends string> = {
key: K;
value: V;
};
*/5. Real-World Architecture: State Store Pattern#
Default generic arguments are heavily utilized in enterprise state management libraries (such as Redux Toolkit or Zustand) to provide clean default signatures while supporting custom action types:
interface BaseAction {
type: string;
}
// TState is required, TAction defaults to BaseAction
class StateStore<TState, TAction extends BaseAction = BaseAction> {
private state: TState;
constructor(initialState: TState) {
this.state = initialState;
}
public getState(): TState {
return this.state;
}
public dispatch(action: TAction): void {
console.log(`[Dispatching Action]: ${action.type}`);
}
}
// Usage 1: Simple Store (uses default BaseAction)
const simpleStore = new StateStore({ count: 0 });
simpleStore.dispatch({ type: "INCREMENT" });
// Usage 2: Strict Store with Discriminated Action Union
type CustomAction = { type: "SET_COUNT"; payload: number } | { type: "RESET" };
const strictStore = new StateStore<{ count: number }, CustomAction>({ count: 0 });
strictStore.dispatch({ type: "SET_COUNT", payload: 5 });
// ❌ Compiler Error: Type '"UNKNOWN_ACTION"' is not assignable to type 'CustomAction'.
// strictStore.dispatch({ type: "UNKNOWN_ACTION" });
Summary & Next Steps#
In this episode:
- We learned how Default Generic Arguments (
<T = Fallback>) eliminate required type verbosity. - We combined constraints with defaults:
<T extends Bound = Default>. - We enforced the Parameter Ordering Rule (Required generics must precede optional generics).
- We implemented production State Stores with default action parameters.
In Episode 32: Multiple Generic Parameters, we will explore managing multiple interacting generic type parameters in complex functions!

