C.W.K.
Stream
Lesson 05 of 07 · published

Default Type Parameters

~7 min · generics, defaults, ergonomics

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Make the common case write less."

The syntax

Default type parameters use the same = syntax as default function parameters: type Result<T, E = Error> says "if the caller doesn't supply E, use Error." The default applies only when the caller leaves the slot unfilled.

This is purely an ergonomic feature — every default is also expressible by writing the type out at call sites. Defaults exist to make 80% cases concise without giving up the 20%.

Common uses

  • Error type defaults to Error: type Result<T, E = Error>
  • State type defaults to empty object: type Action<S = {}>
  • Element type defaults to unknown: type Container<T = unknown>
  • Component props defaults to empty: type Component<P = {}>

Constraints + defaults

You can combine: <T extends Constraint = Fallback>. The default must satisfy the constraint. If it doesn't, the compiler errors at the declaration, not the call site. This is how libraries provide both safety (via constraints) and convenience (via defaults).

Default type parameters trade flexibility for brevity in the common case. Use them when there's an obvious default and most callers will use it. Don't overuse — too many defaults make the type's intent unclear.

Code

Defaults make common cases concise·typescript
// Default — concise common case.
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parse(s: string): Result<number> {  // E defaults to Error
  const n = Number(s);
  if (Number.isNaN(n)) return { ok: false, error: new Error('NaN') };
  return { ok: true, value: n };
}

// Override the default when needed.
type CustomError = { code: number; msg: string };
function parseCustom(s: string): Result<number, CustomError> {
  // ...
  return { ok: false, error: { code: 1, msg: 'parse error' } };
}

// Constraint + default — the library pattern.
function event<T extends Event = Event>(handler: (e: T) => void) { /* ... */ }
event((e) => e.preventDefault());          // T = Event
event<MouseEvent>((e) => e.clientX);       // T = MouseEvent, narrowed

External links

Exercise

Define type ApiResponse<T, E = string> where E is an error message string by default. Then write a function that returns ApiResponse<User> for success and one that returns ApiResponse<User, ValidationError> for explicit overrides. Confirm both type-check.
Hint
When you don't pass E, it falls back to string. When you do, the constraint (none here, but you could add E extends ...) limits what you can pass.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.