Chapter 5.5☕ 13 min read

Default Type Parameters

Set a default type so the common case is effortless

01The Default Shawarma

Sometimes a generic type has a "most common" type that you use 90% of the time. Instead of specifying it every single time, you can set a default — if the caller doesn't provide a type, the default kicks in automatically.

Think of ordering shawarma at a Charminar street stall. The default is chicken shawarma — most people order it. You don't have to say "chicken shawarma" every time; just "bhai, ek shawarma" and you get chicken. BUT, if you want paneer or mutton, you specify: "Bhai, paneer shawarma!"

Default type parameters work exactly the same way — ApiResponse<T = string> means if you just say ApiResponse, T defaults to string. But if you say ApiResponse<User>, T becomes User. The default makes the common case easy and the special case still possible.

This is incredibly useful in real codebases. Imagine an API response wrapper — most endpoints return string error messages, so T = string as default saves you from typing ApiResponse<string> everywhere. But for that one endpoint returning a User object, you simply write ApiResponse<User>. Without defaults, every usage must be explicit, even when 90% of usages are identical. Defaults eliminate that repetition while preserving full flexibility.

02Default Type Syntax

The syntax for default type parameters is beautifully simple: add = DefaultType after the type parameter name. This tells TypeScript: "if nobody provides this type, use this one instead." It works across interfaces, functions, classes, and type aliases — anywhere generics live.

Interface with default:

interface ApiResponse<T = string> {
  data: T;
  status: number;
}

// T defaults to string
type Default = ApiResponse;
// ApiResponse<string>

// T is overridden to User
type WithUser = ApiResponse<User>;

Function with default:

function createBox<T = string>(
  value?: T
): { value: T } {
  return { value: value as T };
}

// T defaults to string
const box1 = createBox();
// T is number
const box2 = createBox<number>(42);

Class with default:

class Store<T = any> {
  private items: T[] = [];
  add(item: T) {
    this.items.push(item);
  }
}

// T defaults to any
const s1 = new Store();
// T is User
const s2 = new Store<User>();

Multiple defaults:

interface Pair<T = string, U = number> {
  first: T;
  second: U;
}

// Pair<string, number>
type P1 = Pair;
// Pair<boolean, number>
type P2 = Pair<boolean>;
// Pair<boolean, boolean>
type P3 = Pair<boolean, boolean>;

Notice how Pair<boolean> only overrides the first type parameter. The second one keeps its default. This left-to-right filling behavior is consistent and predictable.

Defaults referencing earlier params:

// U defaults to an array of T!
interface Result<T, U = T[]> {
  value: T;
  items: U;
}

// Result<string, string[]>
type R1 = Result<string>;
// Result<number, boolean>
type R2 = Result<number, boolean>;

This is a powerful pattern — the default for one type parameter can depend on another! Here, if you don't specify U, it becomes T[]. So Result<string> automatically gets string[] for items. This reduces boilerplate in APIs where secondary types are derived from primary ones.

03Rules & Constraints

Default type parameters come with specific rules that you must follow. Understanding these prevents confusing compiler errors and helps you design clean, predictable generic APIs. Let's walk through each rule carefully.

Rule 1: Defaults make parameters optional.

A type parameter with a default does NOT need to be provided — it's optional. If you don't pass it, the default is used. This is the whole point of having a default in the first place!

Rule 2: Required before optional.

Required type parameters must come BEFORE optional (default) ones, just like function parameters. This is a hard rule in TypeScript:

// ✅ Valid: required first
interface Good<T, U = T[]> {
  value: T;
  items: U;
}

// ❌ Invalid: optional before
// required
interface Bad<T = string, U> {
  value: T;
  items: U;
}

Rule 3: Mix constraints with defaults.

You can combine extends constraints with default values. The constraint restricts what types are allowed, and the default provides a fallback within those constraints:

// T must be string, default "hello"
type Greet<T extends string = "hello">;

// T must be object, default Record
type Data<
  T extends object =
    Record<string, unknown>
>;

Rule 4: Defaults can be any type.

Primitives, objects, other type parameters, even never or unknown — any type can serve as a default. The only requirement is that it must satisfy any constraint on the parameter.

Rule 5: Types fill left to right.

When you provide SOME type parameters, they fill from LEFT to RIGHT. You can't skip a parameter and only override a later one:

interface Pair<T = string, U = number> {
  first: T;
  second: U;
}

// ✅ Override first, keep default
// for second
type A = Pair<boolean>;
// Pair<boolean, number>

// ❌ No syntax to skip first
// and only override second!
// Pair<?, boolean> — INVALID

// ✅ Must specify both to
// override the second
type B = Pair<string, boolean>;
// Pair<string, boolean>
04Default Type Traps

Even experienced developers trip over these default type parameter gotchas. Let's make sure you avoid every single one of them!

Trap 1: Default before required parameter.

This is the #1 mistake. Putting an optional (default) parameter before a required one causes a compiler error. TypeScript enforces the same rule as function parameters — required must come first:

// ❌ ERROR! Required type params
// may not follow optional ones
interface Bad<T = string, U> {
  value: T;
  items: U;
}

// ✅ Fix: required first
interface Good<T, U = string> {
  value: T;
  items: U;
}

Trap 2: Defaults are NOT runtime values.

This is a conceptual trap. <T = string> means the TYPE defaults to string. It does NOT create a string value at runtime. It's purely a compile-time type-level default. Your code still needs to provide actual values. Think of it this way: the default tells TypeScript what shape to expect, not what data to create.

Trap 3: Default must satisfy constraint.

If you have a constraint and a default, the default must satisfy the constraint. Otherwise, TypeScript gives an error:

// ❌ ERROR! string doesn't extend
// number
type Bad<T extends number = string>;

// ✅ 42 (literal) extends number
type Good<T extends number = 42>;

Trap 4: Over-relying on defaults.

If everyone using your API always specifies the type anyway, the default adds no value and can be confusing. Only add defaults when the default is genuinely useful — when the majority of call sites benefit from not having to specify the type. A default that nobody uses is dead code in your type signature.

Trap 5: Default doesn't affect inference.

function wrap<T = string>(val: T) {
  return { value: val };
}

// T inferred as string (default
// kicks in, no argument)
const a = wrap();
// { value: string }

// T inferred as "hello" literal,
// NOT just string — default
// doesn't override inference!
const b = wrap("hello");
// { value: "hello" }

When you provide an argument, TypeScript infers the type from the argument, not from the default. The default only applies when there's nothing to infer from. This is correct behavior, but it can surprise you if you expect the default to "widen" the inferred type.

05Default Types Cheatsheet

Here's your quick-reference cheatsheet for default type parameters — pin it to your mental dashboard and refer back whenever you need it!

  • Basic syntax: <T = DefaultType>
  • Multiple defaults: <T, U = T>
  • With constraint: <T extends X = DefaultX>
  • Required before optional: <T, U = string> ✅, <T = string, U>
  • Default must satisfy constraint: <T extends number = 42> ✅, <T extends number = string>
  • Types fill left to right: Pair<boolean> overrides first only
  • Type-level only: Defaults affect types, not runtime values
  • Use when: One type is overwhelmingly common and specifying it every time is tedious
// Quick reference examples
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

type A = ApiResponse;
// ApiResponse<string>

type B = ApiResponse<User>;
// ApiResponse<User>

interface Result<T, U = T[]> {
  value: T;
  items: U;
}

type C = Result<number>;
// Result<number, number[]>

The Golden Rule: Default type parameters are the shawarma stall's default chicken — order without specifying and you get the usual. Want something different? Just say it, bhai!

Key Takeaways

  • Default type parameters let you set a fallback type when the caller doesn't specify one
  • Syntax: <T = DefaultType> — just like function default parameters
  • Required parameters must come before optional (default) ones
  • The default type must satisfy any constraint on the parameter
  • Defaults are type-level only — they have no runtime effect
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase