Engineering Notes
Home/Languages/TypeScript/Generic Constraints
Languages · TypeScript

Generic Constraints

An unconstrained T could be literally any type — a number, a boolean, anything with no .length at all. The checker has no basis to allow .length access unless something guarantees T will always have it.

💻 Languages● Advanced⏱ 5 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • The Problem: Unconstrained Generics Can't Do Anything
  • extends as a Constraint
  • Constraining One Type Parameter by Another
  • Constraints With Defaults
  • Constraining to a Union of Literals

1The Problem: Unconstrained Generics Can't Do Anything

ts
function getLength<T>(item: T): number {
  return item.length; // ❌ error: Property 'length' does not exist on type 'T'
}

An unconstrained T could be literally any type — a number, a boolean, anything with no .length at all. The checker has no basis to allow .length access unless something guarantees T will always have it.

2extends as a Constraint

ts
function getLength<T extends { length: number }>(item: T): number {
  return item.length; // ✅ — every valid T is guaranteed to have .length
}

getLength("hello");     // ✅ strings have .length
getLength([1, 2, 3]);    // ✅ arrays have .length
getLength(42);            // ❌ numbers don't have .length — rejected at the CALL SITE

T extends U here restricts which types are allowed to be substituted for T at all — any concrete type argument must be assignable to the constraint, exactly the same assignability check used everywhere else in the type system (Type System Fundamentals topic).

3Constraining One Type Parameter by Another

ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Ana" };
getProperty(user, "name"); // ✅ "name" is a valid key of user's type
getProperty(user, "email"); // ❌ "email" isn't a key of user's type — caught at compile time

This is the real, complete signature for getProperty from the Keyof/Typeof Operators topic's exercise — K extends keyof T ties the second type parameter's valid values directly to the first one's actual keys, which is what makes the key argument fully type-checked.

4Constraints With Defaults

ts
interface Fetcher<T extends object = Record<string, unknown>> {
  fetch(): Promise<T>;
}

A type parameter can have both a constraint (what's allowed) and a default (what's used if nothing is specified) simultaneously — the default itself must also satisfy the constraint.

5Constraining to a Union of Literals

ts
function setStatus<T extends "active" | "inactive" | "pending">(status: T): void {
  console.log(status);
}

setStatus("active");  // ✅
setStatus("deleted"); // ❌ not one of the allowed literals
Under the HoodWhat a Constraint Actually Changes Inside the Function Body▾

Inside the generic function, T is treated as its constraint, not as an arbitrary unknown

ts
function getLength<T extends { length: number }>(item: T): number {
  const len: number = item.length; // the checker treats `item` as having AT LEAST { length: number }
  return len;
}

Within the body, the checker doesn't know exactly what T will be at any given call site — but it knows every valid T is assignable to the constraint, so it's always safe to treat item as having (at minimum) the constraint's shape. This is the precise mechanical reason .length becomes accessible: the constraint acts as a lower bound on what the function body can assume T provides.

The constraint is checked again at every call site, independently

ts
function process<T extends { id: number }>(item: T): T {
  return item;
}

process({ id: 1 });             // ✅ checked here — satisfies constraint
process({ id: 2, name: "x" });    // ✅ checked here too — extra properties fine, structural typing
process({ name: "no id" });        // ❌ checked here — fails, missing id

Each call site independently verifies its own argument against the constraint — this is the same "is the source assignable to the target" check used throughout the type system, just run once per call rather than once for the whole generic definition, because T is genuinely different (potentially) at each call.

A constraint doesn't narrow T down to a single type — it only bounds it from below

ts
function wrap<T extends { id: number }>(item: T): T {
  return { ...item, wrapped: true } as T; // ❌ this actually breaks — T might have MORE required
                                            // properties than { id: number }, which the object
                                            // literal above doesn't necessarily provide
}
The constraint {length: number} is the minimum guaranteed shape; the real T at any call site could be much bigger
The constraint {length: number} is the minimum guaranteed shape; the real T at any call site could be much bigger

This is a genuinely common generic-constraints trap: the constraint only guarantees T has at least the constrained shape — T could be a much more specific, larger type at any real call site. The function body cannot assume it knows T's entire shape just because it knows the constraint; only what the constraint itself specifies is safe to rely on.

Constraints participate in inference, not just validation

When a type parameter's actual value would be inferred from an argument (Generics topic's unification), the constraint also acts as a fallback: if there's nothing to infer T from at a given call site, the checker uses the constraint itself as T's type, rather than falling back to unknown or erroring immediately.

⚠️ Common Pitfalls

  1. Forgetting the constraint is a lower bound, not an exact shape — see section 6's wrap example; code inside the function body cannot assume T has only the constrained properties.
  2. Using an overly loose constraint (or none at all) and then fighting the checker inside the function body — usually a sign the constraint needs tightening to match what the implementation actually needs to do.
  3. Confusing T extends U as a constraint (generics) with T extends U ? X : Y as a conditional type — same keyword, genuinely different meaning depending on context; the Conditional Types topic covers the other usage.
  4. Constraining too tightly and losing reusability — an overly specific constraint (e.g. requiring an exact interface instead of just the properties actually used) defeats generics' purpose of working across many compatible shapes.

📋 Quick Reference

text
function f<T extends U>(x: T)         → T must be assignable to U to be used as a type argument
function f<T, K extends keyof T>(...)   → ties one type parameter's valid values to another's keys
inside the body                          → T treated as AT LEAST its constraint, never exactly it
each call site                             → constraint re-checked independently against the real argument

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What is a generic constraint?core▾

Restricting a type parameter with extends so you can rely on certain properties: function longest<T extends { length: number }>(a: T, b: T) works for strings and arrays.

Follow-ups interviewers ask
  • What happens without it? Accessing .length would be an error.
Q2How do constraints referencing other parameters work?core▾

<T, K extends keyof T> ties K to T's keys; <T extends Base, U extends T> builds relationships between parameters.

Follow-ups interviewers ask
  • Use case? Typed pluck, update or event emitter functions.
Q3What's the difference between T extends object and T extends {}?senior▾

object excludes primitives; {} accepts any non-null/undefined value including primitives. unknown accepts everything.

Follow-ups interviewers ask
  • Which for 'any object'? Record<string, unknown> or object.
Q4How do constraints help with React components?core▾

function Select<T extends { id: string }>(props: { options: T[]; onChange: (v: T) => void }) ensures options have ids and onChange receives the precise option type.

Follow-ups interviewers ask
  • Benefit? Callers get their own types back.
Q5What's a common constraint mistake?core▾

Over-constraining (forcing concrete types and losing inference) or using constraints where a simple parameter type would do.

Follow-ups interviewers ask
  • Signs? Generics that appear once or never change the outcome.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.