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 defaultE = Errorlets them writeResult<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:
| Default | When to use |
|---|---|
unknown | External data, untyped sources |
any | Legacy interop (avoid if possible) |
string | Text-oriented APIs |
number | Numeric APIs |
void | Callbacks with no return |
never | Empty by default |
object | Generic 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
unknownis a good default: It’s the top type — every value is assignable to it. It doesn’t force any shape. And unlikeany, 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
Tcan’t be inferred and there’s no default, the caller must always specify it. That’s friction. A default likestringorunknowngives 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 thanResult<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>bindsAto the first parameter andBto 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 errorResult<T, E = Error>— error defaults toErrorCallback<T = void, E = Error>— no payload by defaultClient<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;Clientalone covers the generic.Callbackis 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
| Form | Meaning |
|---|---|
<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
| Location | Example |
|---|---|
| Type alias | type Box<T = string> |
| Interface | interface Container<T = unknown> |
| Class | class Stack<T = unknown> |
| Function | function create<T = string>() |
| Method | method<T = string>() |
Common Defaults
| Default | Use case |
|---|---|
unknown | Untyped external data |
any | Legacy (avoid) |
string | Text APIs |
number | Numeric APIs |
void | Callback return |
never | Empty |
object | Any object |
{} | Any non-null |
Error | Error type |
T | Same as another param |
Ordering Rules
| Rule | Meaning |
|---|---|
| Required first | Defaults come after |
| Defaults last | Can’t precede required |
| Multiple defaults | Allowed at the end |
| Referenced defaults | Can use earlier params |
T = U | U must be earlier |
Inference vs Defaults
| Situation | Result |
|---|---|
| Argument infers T | Inference wins |
| Type argument given | Explicit wins |
| Nothing to infer | Default applies |
| No arguments | Default applies |
Defaults + Constraints
| Form | Valid? |
|---|---|
<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
| Form | Meaning |
|---|---|
<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
| Pattern | Signature |
|---|---|
| Result | type Result<T, E = Error> |
| Response | type Response<T = unknown> |
| Callback | type Callback<T = void> |
| Cache | class Cache<K, V = unknown> |
| Pair | class Pair<A, B = A> |
| Optional | type Optional<T, K extends keyof T = keyof T> |
| Box | class Box<T = unknown> |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Required type parameters may not follow optional | Default before required | Reorder |
Default does not satisfy constraint | Default violates extends | Fix default |
Type argument not provided | No default and no inference | Add default |
Expected N type arguments | Wrong arity | Match parameter count |
When to Add a Default
| Situation | Default? |
|---|---|
| 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
| Pitfall | Problem | Solution |
|---|---|---|
| Default before required | Compile error | Reorder |
| Default violates constraint | Compile error | Fix default |
| Expect default to override inference | Inferred type wins | Understand the rule |
any as default | Unsafe | Use unknown |
| Reference later parameter | Compile error | Reference earlier ones |
| Unused defaults | Confusion | Remove if not useful |
| Wrong default choice | Too narrow or wide | Match use case |
Forgetting extends | Constraint missing | Add if needed |
| Over-parameterized types | Verbose | Add 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
| Concept | Meaning |
|---|---|
| Default type parameter | <T = string> |
| Default with constraint | <T extends U = U> |
| Referencing default | <A, B = A> |
| Ordering rule | Required before default |
| Inference priority | Explicit > inferred > default |
unknown default | Safe fallback |
void default | Callback with no payload |
Error default | Standard 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 defaults —
f<number>(x)overrides the default - Defaults can reference earlier parameters —
<A, B = A> - Use
unknownas a safe default for external data - Use
voidfor callbacks that don’t carry a payload - Use
Errorfor 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!