Lesson 77 of 80
`<T = DefaultType>` lets a generic fall back to a sensible type when the caller doesn't specify one — useful for keeping simple call sites simple, risky when the fallback quietly hides a mistake.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A generic type parameter can have a default, the same way a function parameter can: interface Box<T = unknown> means "if nobody says what T is, assume unknown." This is different from a generic constraint (T extends Shape), which restricts what T is allowed to be — a default only kicks in when T isn't specified or can't be inferred, and it doesn't restrict anything on its own.
Defaults exist to keep the common case ergonomic. A type that's usually used one way shouldn't force every caller to spell out the generic argument explicitly — but the same convenience is exactly what causes the pitfall: when TypeScript can't infer T from context, it silently reaches for the default instead of erroring, and that default might not be what the caller actually meant.
class EventEmitter<Payload = unknown> {
private listeners: Array<(payload: Payload) => void> = [];
on(fn: (payload: Payload) => void) {
this.listeners.push(fn);
}
emit(payload: Payload) {
this.listeners.forEach((fn) => fn(payload));
}
}
const clicks = new EventEmitter<{ x: number; y: number }>();
clicks.on((p) => console.log(p.x, p.y)); // ✅ p is { x: number; y: number }
const generic = new EventEmitter(); // no argument given — Payload defaults to unknown
generic.on((p) => console.log(p.toUpperCase())); // ❌ p is unknown — can't call methods on it
new EventEmitter<{ x: number; y: number }>() explicitly supplies Payload, so everything downstream is precisely typed. new EventEmitter() supplies nothing — TypeScript can't infer Payload from a no-argument constructor call, so it falls back to the default, unknown. That's the safe version of the pitfall: unknown still forces you to narrow before doing anything with p, so the mistake (forgetting to specify the payload type) is caught immediately at the first real use.
unknowninterface FetchOptions<ResponseShape = any> {
url: string;
parse?: (raw: unknown) => ResponseShape;
}
function request<ResponseShape = any>(
opts: FetchOptions<ResponseShape>
): Promise<ResponseShape> {
return fetch(opts.url).then((r) => r.json());
}
// Forgot to specify ResponseShape, and forgot `parse` too:
const data = await request({ url: "/api/users" });
data.nonexistentProperty; // ✅ no error — ResponseShape silently defaulted to `any`
Here the default is any instead of unknown, and that changes everything: data isn't flagged as needing narrowing, it's just treated as fully trusted, anything-goes any. The caller forgot to specify what shape the response has, and instead of an error, they got silent, unchecked access — exactly the failure mode any always causes, just reached via a default instead of an explicit annotation.
T can be inferred from an argument, TypeScript uses the inferred type and ignores the default entirely — defaults are a fallback of last resort, not a preference.<T extends object = {}> constrains what's allowed and supplies a fallback — leaving off either half is a deliberate choice, not an oversight to always "fix."EventEmitter<Payload = unknown> with no argument still produces a fully concrete EventEmitter<unknown> — there's no Payload | undefined involved; the parameter is always some real type, it's just chosen for you when unspecified.This is the direct counterpart to generic constraints — T extends Shape narrows what's allowed, T = DefaultType chooses what's used when nothing is specified, and real-world generic APIs frequently need both together. The any-default pitfall here is also just the generics-flavored version of the any-vs-unknown lesson's core warning: any opts an entire code path out of type checking, whether it arrives via an explicit annotation or, as here, a default nobody thought about.