TypeScript Deep Dives · Part 3
TypeScript generics that stop fighting you
Unconstrained generics often return `any` in a costume. Learn how `extends`, indexed access, and call-site inference make generics disappear for callers, with practical examples.
Generics are for inference, not <T> soup
If your team writes fn<Type>() at every call site, the generic is not helping. It has become boilerplate.
A good generic:
- Constrains what
Tcan be (extends) - Infers from arguments at the call site
- Returns a precise type (often via indexed access)
A bad generic accepts anything and hands back anything, any wearing a costume.
The classic loose helper
// Accepts anything, returns anything
function get(obj: any, key: string): any {
return obj[key];
}
const email = get(user, "email"); // anyCallers get no autocomplete on key and no useful type on the result.
Constrain + return the exact type
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const email = get(user, "email"); // string (or whatever User["email"] is)What changed:
Tis inferred fromobjKis constrained to real keys ofT- The return type is
T[K], notstring | number | …soup
At the call site, the generic disappears. You pass values; TypeScript returns types.
Let inference flow
Parameter order matters. Put the value that carries the type information first, so later parameters can depend on it.
// Harder to infer from later args
function bad<T>(key: keyof T, obj: T) { /* ... */ }
// Natural inference from obj → T → keyof T
function good<T, K extends keyof T>(obj: T, key: K) { /* ... */ }Don’t over-generalize
Generics are a tool for shared structure across multiple types. If a type parameter is used once, and only for one concrete case, delete it.
Signs you overdid it:
- Every function in a file is generic “just in case”
- Callers always pass type arguments manually
- Constraints are missing, so errors show up far from the real mistake
Bottom line
- get(obj: any, key: string): any
+ get<T, K extends keyof T>(obj: T, key: K): T[K]Callers pass values. They get types back. No <T> by hand.
That is what “generics that stop fighting you” means in practice.
FAQ
Why do my generics still feel like `any`?+
If `<T>` is unconstrained and your return type is loosely typed, callers get little safety. Constrain type parameters (for example `K extends keyof T`) and return precise types such as `T[K]`.
Should callers write `fn<Type>(...)` everywhere?+
Usually no. If callers must pass type arguments by hand, the signature is often poorly ordered or under-constrained. Design APIs so earlier arguments infer the types later ones depend on.
When should I avoid generics?+
When only one concrete type exists, or the generic appears in a single place. Prefer a concrete type. Generics used once are often just indirection.
TypeScript workshops for teams
We run generics and inference drills on your own helpers and APIs until the patterns stick for the whole team.
View workshops →Keep learning
TypeScript `satisfies` vs `as`: stop silencing the compiler
`as` isn’t a conversion, it’s a promise. `satisfies` checks the value and keeps the exact inferred shape.
Make TypeScript check your strings: template literal & mapped types
Model routes, events, and keys as types, not `string`, so typos become red squiggles before runtime.
TypeScript 7 is ~10× faster. Why your CI often only gets ~28%
The 10× headline is real on the right hardware. On shared CI, structure still wins: config, types, and project layout.