| |

TypeScript 28 🔷 Default Type Parameters

A default type parameter is a fallback type applied when the caller doesn’t provide a type argument and TypeScript can’t infer one. It’s written after the type parameter with = DefaultType. Defaults make generic types easier to use — the caller only specifies what they care about, and the rest falls back to sensible values. They’re everywhere in the standard library and in well-designed APIs: Promise<T> has no default, but Map<K, V> and Record<K, V> and countless user-defined types use defaults to stay convenient.

Key point: A default applies when the type can’t be inferred and isn’t specified. It doesn’t override inference — if the compiler can figure out T from the arguments, the default is ignored. Defaults are for ergonomics: they let callers write Result<T> instead of Result<T, Error>, or Container<T> without repeating a common type. Order matters: a type parameter with a default must come after all parameters without one.


What a default type parameter is

A default type parameter is written with = DefaultType.

type Box<T = string> = {
  value: T;
};

If the caller doesn’t specify T, it becomes string.

const a: Box = { value: 'hello' };        // T = string (default)
const b: Box<number> = { value: 42 };     // T = number (specified)
const c: Box<boolean> = { value: true };  // T = boolean

Box without arguments is Box<string>. With an explicit argument, the default is ignored.

Where defaults apply:

  • Generic type aliases
  • Generic interfaces
  • Generic classes
  • Generic functions
  • Generic methods
interface Comparable<T = unknown> {
  compareTo(other: T): number;
}

class Stack<T = unknown> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
}

function create<T = string>(): T[] {
  return [] as T[];
}

Each has a fallback for when the type isn’t provided.

What a default does not do:

  • It doesn’t override inference — inferred types win
  • It doesn’t apply when a type is explicitly specified
  • It doesn’t loosen constraints — the default must satisfy any constraint

When defaults kick in:

  • No inference is possible (e.g., create() with no arguments)
  • No explicit type is given

Otherwise, the caller’s type or the inferred type wins.

Why defaults exist: Generic types with multiple parameters are verbose. Result<T, E> requires two types every time — even when the caller only cares about the value. A default E = Error lets them write Result<T> and get the common case. Defaults make generics convenient without losing flexibility.


Default with no constraint

The simplest case: a default that’s just a fallback type.

type Response<T = unknown> = {
  data: T;
  status: number;
};

const a: Response = { data: 'anything', status: 200 };  // T = unknown
const b: Response<User> = { data: user, status: 200 };  // T = User

unknown is a common default — it means “we don’t know the type” without giving up safety like any would.

Common default choices:

DefaultWhen to use
unknownExternal data, untyped sources
anyLegacy interop (avoid if possible)
stringText-oriented APIs
numberNumeric APIs
voidCallbacks with no return
neverEmpty by default
objectGeneric object shape
{}Any non-null value

Why unknown over any: unknown forces narrowing before use. As a default, it means “the caller didn’t specify a type, and we’re not going to assume anything.” That’s safer than any, which would let the caller use the value as anything.

Why unknown is a good default: It’s the top type — every value is assignable to it. It doesn’t force any shape. And unlike any, it requires narrowing before use. That’s the right balance for a default: flexible without being unsafe.


Default with a constraint

A default must satisfy any constraint on the type parameter.

class Result<T, E extends Error = Error> {
  constructor(
    public ok: boolean,
    public value?: T,
    public error?: E
  ) {}
}

E extends Error is the constraint; = Error is the default. Error satisfies Error.

Default must match constraint:

// ✅ Default satisfies constraint
class A<T extends object = object> { }

// ❌ Default violates constraint
class B<T extends object = string> { }
// Error: Type 'string' does not satisfy the constraint 'object'.

Default equal to constraint:

The most common pattern — the default is exactly the constraint.

function create<T extends object = object>(): T {
  return {} as T;
}

const a = create();                    // T = object
const b = create<{ id: number }>();    // T = { id: number }

Default as a specific subtype:

interface Event {
  type: string;
}

class Emitter<E extends Event = Event> {
  private handlers: Array<(e: E) => void> = [];

  on(handler: (e: E) => void): void {
    this.handlers.push(handler);
  }

  emit(e: E): void {
    this.handlers.forEach(h => h(e));
  }
}

const e1 = new Emitter();                 // E = Event
const e2 = new Emitter<{ type: 'click' }>(); // E = { type: 'click' }

The default is the constraint itself — the most general valid type.

Default referencing another parameter:

A default can reference earlier type parameters.

class Pair<A, B = A> {
  constructor(public first: A, public second: B) {}
}

const p1 = new Pair(1, 'a');    // Pair<number, string>
const p2 = new Pair(1, 1);      // Pair<number, number> — B defaults to A
const p3: Pair<number> = new Pair(1, 2);  // B = number (default = A)

B = A means “B defaults to whatever A is.” If both arguments are numbers, A and B are both number. If the caller explicitly types Pair<number>, B becomes number.

Why reference another parameter: It lets the default adapt to the caller’s context. Pair<number> means both values are numbers by default; Pair<number, string> overrides.

Why defaults must satisfy constraints: The constraint is a requirement; the default is a value for the type parameter. If the default didn’t satisfy the constraint, the type would be invalid whenever the default applied. TypeScript checks this at the declaration — you can’t ship a type that might fail its own constraint.


Defaults on functions

A function can have default type parameters.

function create<T = string>(): T[] {
  return [] as T[];
}

const a = create();              // T = string
const b = create<number>();      // T = number

Since create has no arguments, there’s nothing to infer from — the default applies. Without a default, the caller would have to specify T every time.

Function with one inferable and one defaulted parameter:

function map<T, U = T>(items: T[], fn: (item: T) => U): U[] {
  return items.map(fn);
}

const a = map([1, 2, 3], n => n * 2);      // U = number (inferred)
const b = map([1, 2, 3], n => `${n}`);     // U = string (inferred)

Here U is inferred from the callback’s return type. The default U = T only applies if inference can’t figure it out — which is rare in this case.

Function with a default that isn’t inferable:

function parse<T = unknown>(json: string): T {
  return JSON.parse(json) as T;
}

const a = parse('{"x":1}');           // T = unknown
const b = parse<{ x: number }>('{"x":1}');  // T = { x: number }

parse can’t infer T from anything — the input is always a string. The default unknown gives a safe fallback when the caller doesn’t specify.

Why defaults on functions are less common: Functions usually infer types from arguments. Defaults are useful when:

  • No argument can determine the type (create<T>() with no args)
  • The caller wants a fallback without specifying
  • There’s a common type the function can assume

Arrow function with default:

const make = <T = string>(): T[] => [];

const a = make();             // string[]
const b = make<number>();     // number[]

Why functions with no inferable types need defaults: If T can’t be inferred and there’s no default, the caller must always specify it. That’s friction. A default like string or unknown gives a sensible baseline; callers who want something else specify it.


Defaults on interfaces and types

Generic interfaces and type aliases support defaults too.

interface Container<T = unknown> {
  add(item: T): void;
  all(): T[];
}

class Box implements Container<string> {
  private items: string[] = [];
  add(item: string): void { this.items.push(item); }
  all(): string[] { return this.items; }
}

Container defaults to unknown. Implementations can fix the type by specifying it.

Defaults in complex types:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

type ApiResponse<T, E = Error> = {
  data?: T;
  error?: E;
  status: number;
};

type Callback<T = void> = (value: T) => void;

Each default provides a fallback for common cases. Result<T> is Result<T, Error>; Callback without a type is Callback<void>.

Defaults in utility-type-like aliases:

type Optional<T, K extends keyof T = keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// All keys optional
type AllOptional = Optional<User>;

// Only 'email' optional
type EmailOptional = Optional<User, 'email'>;

K defaults to keyof T — all keys. When the caller provides a specific key, the default is replaced.

Why defaults in type aliases matter: They make the type usable without full parameterization. Result<T> and Callback are common; forcing callers to write Result<T, Error> everywhere is friction. Defaults remove it.

Why defaults are especially useful in type aliases: Type aliases are often used as shorthand for complex types. Defaults let a short name cover the common case and accept parameters when needed. Result<T> is nicer than Result<T, Error> for most uses, and it’s still overridable.


Ordering rules

Type parameters with defaults must come after those without.

// ✅ Valid
type A<T, U = string> = { t: T; u: U };

// ❌ Invalid
type B<T = string, U> = { t: T; u: U };
// Error: Required type parameters may not follow optional type parameters.

Why the rule: Type parameters are positional. If a defaulted parameter comes first, the caller can’t skip it to provide the later one. B<string, number> would be ambiguous — does the first argument go to T or U?

Correct ordering patterns:

// All required, then all optional
type Result<T, E = Error> = ...;

// Multiple defaults
type Cache<K, V = unknown, E = Error> = ...;

// Required with defaults
function f<T, U = T>(a: T, b: U): [T, U] { return [a, b]; }

When defaults are mixed with required parameters: Required parameters come first so the caller can always provide them positionally. Defaults fill the tail.

The rule in practice: You rarely hit this error. But if you do, reorder so required parameters come first.

Why the rule exists: TypeScript resolves type arguments positionally. F<A, B> binds A to the first parameter and B to the second. If the first had a default and the second didn’t, there’d be no way to specify only the second. The ordering rule makes this impossible by design.


Defaults don’t override inference

A default applies only when the type can’t be inferred. If inference can figure out T, the default is ignored.

function identity<T = string>(x: T): T {
  return x;
}

const a = identity(42);         // T = number (inferred)
const b = identity('hello');    // T = string (inferred)
const c = identity<number>(42); // T = number (specified)

T = string is the default, but calling identity(42) infers T = number from the argument. The default only applies if the argument is missing — which can’t happen here.

When defaults actually apply:

function create<T = string>(): T[] {
  return [];
}

const a = create();     // T = string (default)

No arguments — nothing to infer from. The default applies.

Inference beats defaults:

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

wrap(42);            // T = number (inferred, default ignored)

Even though T = string is declared, the argument 42 infers number. The default is a fallback, not a preference.

When there’s no argument:

function empty<T = string>(): T[] {
  return [];
}

const a = empty();            // T = string
const b = empty<number>();    // T = number

No inference source, so the default applies (or the explicit type wins).

Why defaults are fallbacks, not preferences: The default exists for when there’s no better information. If the caller provides a value or a type, that’s better information. Defaults only kick in when inference has nothing to work with — like when a function has no arguments that mention T.


A full example

A type-safe API client using default type parameters.

// ============================================
// TYPES WITH DEFAULTS
// ============================================

interface ApiResponse<T = unknown, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
  status: number;
}

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

type Callback<T = void, E = Error> = (result: Result<T, E>) => void;

class Client<D = unknown, E = Error> {
  constructor(private baseUrl: string) {}

  async get<T = D>(path: string): Promise<ApiResponse<T, E>> {
    const res = await fetch(`${this.baseUrl}${path}`);
    const data = await res.json();
    return {
      ok: res.ok,
      data: res.ok ? data : undefined,
      error: res.ok ? undefined : (data as E),
      status: res.status
    };
  }

  async request<T = D>(
    path: string,
    callback: Callback<T, E>
  ): Promise<void> {
    const response = await this.get<T>(path);
    if (response.ok && response.data !== undefined) {
      callback({ ok: true, value: response.data });
    } else {
      callback({ ok: false, error: response.error! });
    }
  }
}

// ============================================
// DOMAIN
// ============================================

interface User {
  id: number;
  name: string;
  email: string;
}

interface ApiError {
  code: number;
  message: string;
}

// ============================================
// USAGE
// ============================================

// Default types — unknown and Error
const generic = new Client('https://api.example.com');
// Client<unknown, Error>

// Specific types
const users = new Client<User, ApiError>('https://api.example.com');
// Client<User, ApiError>

async function loadUser(id: number): Promise<User | null> {
  const res = await users.get(`/users/${id}`);
  if (res.ok && res.data) {
    return res.data;   // User
  }
  return null;
}

async function fetchUser(id: number, cb: Callback<User, ApiError>): Promise<void> {
  await users.request(`/users/${id}`, cb);
}

// Callbacks with defaults
const log: Callback = (result) => {
  if (result.ok) {
    // result.value is void
    console.log('Done');
  } else {
    console.error(result.error.message);
  }
};

const logUser: Callback<User> = (result) => {
  if (result.ok) {
    console.log(result.value.name);  // User
  }
};

What this shows:

  • ApiResponse<T = unknown, E = Error> — defaults for data and error
  • Result<T, E = Error> — error defaults to Error
  • Callback<T = void, E = Error> — no payload by default
  • Client<D = unknown, E = Error> — class with defaults
  • Methods can override with their own defaults (get<T = D>)

Every type has a sensible default. Callers specify only what they care about.

Why this shape: It’s how real API clients are typed. Client<User, ApiError> covers the specific case; Client alone covers the generic. Callback is convenient without a payload and precise with one. Defaults make the types ergonomic without sacrificing safety.


Complete Example Session

# ============================================
# PART 1: BASIC DEFAULT
# ============================================

cat > basic.ts << 'EOF'
type Box<T = string> = { value: T };

const a: Box = { value: 'hello' };       // T = string
const b: Box<number> = { value: 42 };    // T = number

console.log(a.value, b.value);
EOF

npx tsc --noEmit basic.ts
# (no errors)

# ============================================
# PART 2: DEFAULT WITH CONSTRAINT
# ============================================

cat > constraint.ts << 'EOF'
class Result<T, E extends Error = Error> {
  constructor(
    public ok: boolean,
    public value?: T,
    public error?: E
  ) {}
}

const a = new Result<number>(true, 42);           // E = Error
const b = new Result<number, TypeError>(false, undefined, new TypeError('x'));

console.log(a.ok, b.error?.message);
EOF

npx tsc --noEmit constraint.ts
# (no errors)

# ============================================
# PART 3: DEFAULT REFERENCING ANOTHER PARAMETER
# ============================================

cat > reference.ts << 'EOF'
class Pair<A, B = A> {
  constructor(public first: A, public second: B) {}
}

const p1 = new Pair(1, 2);       // Pair<number, number>
const p2 = new Pair(1, 'a');     // Pair<number, string>

console.log(p1, p2);
EOF

npx tsc --noEmit reference.ts
# (no errors)

# ============================================
# PART 4: DEFAULT ON A FUNCTION
# ============================================

cat > fn.ts << 'EOF'
function create<T = string>(): T[] {
  return [] as T[];
}

const a = create();            // string[]
const b = create<number>();    // number[]

console.log(a, b);
EOF

npx tsc --noEmit fn.ts
# (no errors)

# ============================================
# PART 5: INFERENCE WINS OVER DEFAULT
# ============================================

cat > infer.ts << 'EOF'
function wrap<T = string>(value: T): { value: T } {
  return { value };
}

const a = wrap(42);       // T = number (inferred)
const b = wrap('hello');  // T = string

console.log(a, b);
EOF

npx tsc --noEmit infer.ts
# (no errors)

# ============================================
# PART 6: ORDERING ERROR
# ============================================

cat > order.ts << 'EOF'
type Bad<T = string, U> = { t: T; u: U };
EOF

npx tsc --noEmit order.ts
# [ order.ts:1:9 - Required type parameters may not follow optional type parameters. ]

rm order.ts

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc basic.ts constraint.ts reference.ts fn.ts infer.ts
node basic.js
# [ hello 42 ]

node constraint.js
# [ true x ]

node reference.js
# [ Pair { first: 1, second: 2 } Pair { first: 1, second: 'a' } ]

node fn.js
# [ [] [] ]

node infer.js
# [ { value: 42 } { value: 'hello' } ]

Quick Reference

Default Syntax

FormMeaning
<T = string>Default to string
<T = unknown>Default to unknown
<T extends U = U>Default to constraint
<T extends U = Default>Default satisfies constraint
<A, B = A>Default references earlier param
<T, U = T>U defaults to T

Where Defaults Apply

LocationExample
Type aliastype Box<T = string>
Interfaceinterface Container<T = unknown>
Classclass Stack<T = unknown>
Functionfunction create<T = string>()
Methodmethod<T = string>()

Common Defaults

DefaultUse case
unknownUntyped external data
anyLegacy (avoid)
stringText APIs
numberNumeric APIs
voidCallback return
neverEmpty
objectAny object
{}Any non-null
ErrorError type
TSame as another param

Ordering Rules

RuleMeaning
Required firstDefaults come after
Defaults lastCan’t precede required
Multiple defaultsAllowed at the end
Referenced defaultsCan use earlier params
T = UU must be earlier

Inference vs Defaults

SituationResult
Argument infers TInference wins
Type argument givenExplicit wins
Nothing to inferDefault applies
No argumentsDefault applies

Defaults + Constraints

FormValid?
<T extends object = object>
<T extends object = {}>
<T extends object = string>
<E extends Error = Error>
<T extends A = A>
<T extends A | B = A>

Defaults Referencing Other Params

FormMeaning
<A, B = A>B defaults to A
<K, V = K>V defaults to K
<T, E = Error>E defaults to Error (unrelated)
<T, U = T[]>U defaults to array of T

Common Patterns

PatternSignature
Resulttype Result<T, E = Error>
Responsetype Response<T = unknown>
Callbacktype Callback<T = void>
Cacheclass Cache<K, V = unknown>
Pairclass Pair<A, B = A>
Optionaltype Optional<T, K extends keyof T = keyof T>
Boxclass Box<T = unknown>

Errors and Fixes

ErrorCauseFix
Required type parameters may not follow optionalDefault before requiredReorder
Default does not satisfy constraintDefault violates extendsFix default
Type argument not providedNo default and no inferenceAdd default
Expected N type argumentsWrong arityMatch parameter count

When to Add a Default

SituationDefault?
Common type used 90% of time
Function with no inference source
Error type paired with value type
Callback with no payload
Every call specifies T explicitly❌ (no benefit)
Type always differs per call

Best Practices

Do This:

// Provide sensible defaults for common cases
type Result<T, E = Error> = ...;                           // ✅

// Default to the constraint when useful
class Cache<K, V = unknown> { }                            // ✅

// Use unknown as a safe default
type Response<T = unknown> = { data: T };                  // ✅

// Reference an earlier parameter when related
class Pair<A, B = A> { }                                   // ✅

// Give functions with no inference a default
function create<T = string>(): T[] { return []; }          // ✅

// Default callback payloads to void
type Callback<T = void> = (v: T) => void;                  // ✅

// Order required parameters first
type Good<T, U = string> = { t: T; u: U };                 // ✅

// Use defaults on class type parameters
class Client<D = unknown, E = Error> { }                   // ✅

// Document why a default is the constraint
// "Default is most general valid type"                     // ✅

Don’t Do This:

// Don't put defaults before required parameters
type Bad<T = string, U> = { t: T; u: U };                  // ❌

// Don't use a default that violates the constraint
class Bad<T extends object = string> { }                   // ❌

// Don't use `any` as a default when `unknown` works
type Bad<T = any> = { value: T };                          // ⚠️

// Don't add defaults nobody uses
function f<T = number>(x: T): T { return x; }
// T always inferred — default is dead code                 // ⚠️

// Don't use a specific type as a default when general is better
class Box<T = never> { }  // ⚠️  empty by default            // ⚠️

// Don't assume defaults override inference
function f<T = string>(x: T): T { return x; }
f(42);  // T = number, not string                          // ⚠️

// Don't reference a later parameter
type Bad<A = B, B> = { a: A; b: B };                       // ❌

// Don't use a default that shadows the constraint name
type Bad<T extends X = X> = ...  // ⚠️  if X is a local type  // ⚠️

Common Pitfalls

PitfallProblemSolution
Default before requiredCompile errorReorder
Default violates constraintCompile errorFix default
Expect default to override inferenceInferred type winsUnderstand the rule
any as defaultUnsafeUse unknown
Reference later parameterCompile errorReference earlier ones
Unused defaultsConfusionRemove if not useful
Wrong default choiceToo narrow or wideMatch use case
Forgetting extendsConstraint missingAdd if needed
Over-parameterized typesVerboseAdd defaults

Real-World Examples

1. Result with default error

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

2. Response with unknown data

type Response<T = unknown> = { data: T; status: number };

3. Callback default void

type Callback<T = void> = (value: T) => void;

4. Cache with unknown value

class Cache<K, V = unknown> {
  private store = new Map<K, V>();
  get(k: K) { return this.store.get(k); }
}

5. Pair with same-type default

class Pair<A, B = A> {
  constructor(public first: A, public second: B) {}
}

6. Box with unknown

class Box<T = unknown> {
  constructor(public value: T) {}
}

7. Comparator default

interface Comparator<T = unknown> {
  compare(a: T, b: T): number;
}

8. Factory with default

function create<T = string>(): T[] {
  return [] as T[];
}

9. Parse with default

function parse<T = unknown>(json: string): T {
  return JSON.parse(json) as T;
}

10. Optional with default keys

type Optional<T, K extends keyof T = keyof T> =
  Omit<T, K> & Partial<Pick<T, K>>;

11. API response

interface ApiResponse<T = unknown, E = Error> {
  ok: boolean;
  data?: T;
  error?: E;
}

12. Client with defaults

class Client<D = unknown, E = Error> {
  constructor(private baseUrl: string) {}
}

13. Method with default

class Client<D = unknown> {
  async get<T = D>(path: string): Promise<T> {
    return fetch(path).then(r => r.json());
  }
}

14. Event emitter with default

class Emitter<E extends Event = Event> {
  on(handler: (e: E) => void): void { }
}

15. Store with defaults

class Store<S = unknown, A = unknown> {
  constructor(public state: S) {}
  dispatch(action: A): void { }
}

16. Validation result

type Validation<T, E = string> =
  | { valid: true; value: T }
  | { valid: false; errors: E[] };

17. Tree

class TreeNode<T = unknown> {
  children: TreeNode<T>[] = [];
  constructor(public value: T) {}
}

18. Paginated

type Paginated<T = unknown> = {
  items: T[];
  page: number;
  total: number;
};

19. Id-keyed map

type IdMap<T = unknown, K extends string = string> = Map<K, T>;

20. Fetch options

interface Options<T = unknown, E = Error> {
  onSuccess?: (data: T) => void;
  onError?: (err: E) => void;
}

Visual: Default Application

┌──────────────────────────────────────────────┐
│  type Box<T = string> = { value: T };        │
│                                              │
└──────────────────────────────────────────────┘
                  │
        ┌─────────┼──────────────┐
        │         │              │
        ▼         ▼              ▼
┌──────────┐ ┌──────────┐ ┌────────────┐
│ Box      │ │ Box<     │ │ Box<number>│
│ (no arg) │ │ boolean> │ │            │
│          │ │          │ │            │
│ T=string │ │ T=bool   │ │ T=number   │
│ (default)│ │ (explicit)│ │ (explicit)│
└──────────┘ └──────────┘ └────────────┘

Visual: Default + Constraint

┌──────────────────────────────────────────────┐
│  class Result<T, E extends Error = Error> {  │
│    constructor(                              │
│      public ok: boolean,                     │
│      public value?: T,                       │
│      public error?: E                        │
│    ) {}                                      │
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Constraint: E extends Error                 │
│  Default:    Error                           │
│                                              │
│  Default satisfies constraint ✅             │
│                                              │
└──────────────────────────────────────────────┘

Visual: Inference Beats Default

┌──────────────────────────────────────────────┐
│  function wrap<T = string>(v: T): Box<T> {   │
│    return { value: v };                      │
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  wrap(42)                                    │
│       │                                      │
│       ▼                                      │
│  Argument infers T = number                  │
│  Default (string) is ignored                 │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  wrap<number>(42)                            │
│       │                                      │
│       ▼                                      │
│  Explicit type T = number                    │
│  Default ignored                             │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  No arguments?  (Hypothetical)               │
│       │                                      │
│       ▼                                      │
│  Nothing to infer → Default applies          │
│                                              │
└──────────────────────────────────────────────┘

Visual: Ordering Rule

┌──────────────────────────────────────────────┐
│  ✅ Valid                                    │
│                                              │
│  <T, U = string>                             │
│   ↑    ↑                                     │
│  req  default                                │
│                                              │
│  <T, U, V = number>                          │
│   ↑  ↑    ↑                                  │
│  req req default                             │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  ❌ Invalid                                  │
│                                              │
│  <T = string, U>                             │
│   ↑           ↑                              │
│  default     req                             │
│                                              │
│  Required cannot follow default              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Default Referencing Another Param

┌──────────────────────────────────────────────┐
│  class Pair<A, B = A> {                      │
│    constructor(public first: A, public second: B) {}│
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  new Pair(1, 2)                              │
│       │                                      │
│       ▼                                      │
│  A = number, B = number (default = A)        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  new Pair(1, 'a')                            │
│       │                                      │
│       ▼                                      │
│  A = number, B = string (inferred)           │
│  Default ignored                             │
│                                              │
└──────────────────────────────────────────────┘

Visual: Default on a Function with No Inference

┌──────────────────────────────────────────────┐
│  function create<T = string>(): T[] {        │
│    return [];                                │
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  create()                                    │
│       │                                      │
│       ▼                                      │
│  No arguments → nothing to infer             │
│  Default applies → T = string                │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  create<number>()                            │
│       │                                      │
│       ▼                                      │
│  Explicit type → T = number                  │
│                                              │
└──────────────────────────────────────────────┘

Visual: Common Defaults

┌──────────────────────────────────────────────┐
│  unknown   →  external data                  │
│  Error     →  error type                     │
│  void      →  callback with no payload       │
│  string    →  text-oriented                  │
│  number    →  numeric                        │
│  object    →  any object                     │
│  T         →  same as another param          │
│                                              │
└──────────────────────────────────────────────┘

Visual: Decision Flow

┌──────────────────────────────────────────────┐
│  Is there a common type callers use?         │
│       │                                      │
│       ├── Yes ──► Add default                │
│       │                                      │
│       └── No ──► Skip default                │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Does the function have inference?           │
│       │                                      │
│       ├── Yes ──► Default rarely applies     │
│       │                                      │
│       └── No ──► Default is essential        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Is there a constraint?                      │
│       │                                      │
│       ├── Yes ──► Default must satisfy it    │
│       │                                      │
│       └── No ──► Any default works           │
│                                              │
└──────────────────────────────────────────────┘

Visual: Default + Inference Interaction

┌──────────────────────────────────────────────┐
│  Priority order:                             │
│                                              │
│  1. Explicit type argument                   │
│     f<number>(...)                           │
│                                              │
│  2. Inferred from arguments                  │
│     f(42)                                    │
│                                              │
│  3. Default type parameter                   │
│     f()                                      │
│                                              │
│  Earlier wins                                │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptMeaning
Default type parameter<T = string>
Default with constraint<T extends U = U>
Referencing default<A, B = A>
Ordering ruleRequired before default
Inference priorityExplicit > inferred > default
unknown defaultSafe fallback
void defaultCallback with no payload
Error defaultStandard error type

Key takeaways:

  • A default type parameter<T = Default> — applies when the type can’t be inferred and isn’t specified
  • Defaults must come after required parameters — required first, defaults last
  • A default must satisfy any constraint<T extends U = U> is the common pattern
  • Inference beats defaults — if the compiler can figure out T, the default is ignored
  • Explicit type arguments beat defaultsf<number>(x) overrides the default
  • Defaults can reference earlier parameters<A, B = A>
  • Use unknown as a safe default for external data
  • Use void for callbacks that don’t carry a payload
  • Use Error for error types paired with value types
  • Defaults are most useful on types (Result<T>) and functions with no inference (create<T>())
  • Defaults don’t override inference — they’re fallbacks, not preferences
  • Add a default only if it’s actually used — unused defaults are dead code

Remember: Default type parameters make generics ergonomic. Result<T, E = Error> saves callers from writing Error every time. Pair<A, B = A> keeps pairs consistent by default. create<T = string>() gives functions with no inference a sensible fallback. The rules are simple: required before default, default must satisfy constraint, inference wins when it applies. Use defaults where they reduce friction — skip them where every call specifies the type anyway. Done right, they make generic types feel as easy as non-generic ones.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!