Where Generics Go Wrong
Intermediate · 21 min read · ▶ live playground · ✦ checkpoint
Generics go wrong when the type parameter stops carrying information. The fix is not "learn a bigger generic trick" — it is usually to remove the type parameter, use a simpler type, or make the source of T honest.
A type parameter used once is a smell
The useful generic from 7.1 linked input type to output type: first<T>(arr: T[]): T. The useful constraint from 7.2 preserved a caller's full solved T. The useful wrappers from 7.3 reused T across fields and methods.
When T appears only once, it usually is not doing that work. The TOC's worst version is <T>(x: T): any: T appears on the input, then the return type throws all information away. We name that pattern because you will see it in older code, but we will not run it here. A generic that accepts anything and returns an unchecked value is just the checker being dismissed with extra punctuation.
Here is the cleaner version of the same smell without the unchecked return:
describeGeneric works, but T does not connect two meaningful positions. The function returns string no matter what came in, so unknown says the same thing more honestly: "I can accept any value, but I am not preserving its type."
keep is different. T appears in the parameter and the return type, so the caller's input type flows back out. That is a relationship. Keep the generic.
Generics do not conjure values
The second failure mode is pretending a function can return T when it has no T-shaped value to return. A type parameter describes a type chosen by the caller. If the caller chooses a richer type, your function has to satisfy that richer promise.
function makePlaceholder<T extends { id: string }>(): T {
return { id: "generated" };
}→ playground.ts:2:3 - error TS2322: Type '{ id: string; }' is not assignable to type 'T'.
'{ id: string; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ id: string; }'.
The object satisfies the constraint, but the function promised T. A caller could ask for { id: string; title: string }, and { id: "generated" } does not have a title. The checker is protecting the caller from a promise you cannot keep.
Make the source of T honest instead:
repeat can return T[] because it received a T. makeWithId can return T because the caller provides a builder that produces T. The generic promise has a real source.
Sometimes the simpler type is the better type
Not every flexible function needs a type parameter.
Use a union when there is one operation and one return shape:
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return "ID-" + id.toFixed(0);
}Use overloads when there are a few known call shapes and callers deserve precise return types. Use unknown when the whole point is "I accept arbitrary input and will inspect it before doing anything specific."
The union version is easier to read than a generic like formatId<T extends string | number>(id: T): string, because T does not preserve anything. The overload version is clearer than trying to encode a tiny lookup table with a type parameter. The unknown version is honest about arbitrary input: you must narrow before using it.
A team budget for generics
Use this budget before adding a new type parameter:
- Can I say the relationship out loud? "For some type T, this input becomes that output" is a good sign.
- Does T appear in at least two meaningful places? One appearance is usually a smell.
- Is the function trying to create T without receiving a T-shaped value or a builder? Stop.
- Would a union explain a closed set of inputs more simply?
- Would overloads explain a few known call shapes more clearly?
- Is this boundary really arbitrary input? Prefer
unknownand narrow it. - If a teammate needs a paragraph to understand the signature, spend the complexity somewhere else.
That budget is not anti-generic. It protects the good generics: the ones that preserve caller information, keep containers consistent, and make library APIs feel precise without making callers think about type parameters.
Section 8 moves from type parameters into constants and literal precision: modern constant patterns and the choices for representing fixed sets of values.
Checkpoint
Answer all three to mark this lesson complete