Learn/TypeScript Deep Dives

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.

8 min read
TypeScriptGenericsInference

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 T can 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

ts
// Accepts anything, returns anything
function get(obj: any, key: string): any {
  return obj[key];
}
 
const email = get(user, "email"); // any

Callers get no autocomplete on key and no useful type on the result.

Constrain + return the exact type

ts
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:

  1. T is inferred from obj
  2. K is constrained to real keys of T
  3. The return type is T[K], not string | 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.

ts
// 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

ts
- 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