TypeScript Generic Constraints

A generic function like function identity<T>(value: T): T works with any type at all — which is exactly the problem when you need to do something with that value, like read a .length property or look up a key. Generic constraints let you narrow an unbounded type parameter down to “any type, as long as it has these features,” using the extends keyword. This gives you the safety of specific types with the reusability of generics.

Overview / How it works

By default, a type parameter T in function f<T>(x: T) is constrained only by the implicit upper bound unknown — the compiler knows nothing about x beyond “it is some type.” That means you can’t access x.length, call x.toFixed(), or index into x with a string key, because those operations aren’t guaranteed to exist on every possible type.

A generic constraint changes the upper bound. Writing <T extends Shape> tells the compiler: “T can be any type, but it must be assignable to Shape.” Inside the function body, the compiler now treats every value typed T as having at least the members of Shape, so you can safely access those members. Crucially, the constraint doesn’t change what the caller can pass — any object with a superset of Shape‘s properties still works, and the return type still refers to the caller’s exact type T, not the wider constraint. This is what makes constrained generics more precise than simply typing the parameter as Shape directly: with a plain Shape parameter you lose information about which specific shape was passed in; with T extends Shape you keep it.

Constraints are a compile-time-only concept. TypeScript uses structural typing to check, at every call site, whether the argument’s shape is assignable to the constraint. Once that check passes, the constraint (and every other type annotation) is erased — the emitted JavaScript contains no trace of extends, T, or any type information at all.

Syntax

function functionName<T extends ConstraintType>(param: T): ReturnType {
  // body can safely use members declared on ConstraintType
}
  • T — the type parameter being constrained.
  • extends — the keyword that introduces the constraint (not related to class inheritance here — think “is assignable to”).
  • ConstraintType — any type: an interface, an object type literal, a union, a class, or keyof SomeType.
  • Multiple type parameters can each have their own constraint, and one constraint can reference an earlier type parameter, e.g. <T, K extends keyof T>.
  • A constraint can be combined with a default: <T extends object = {}> supplies a fallback type when the caller omits the type argument and it can’t be inferred.
Constraint form Meaning
T extends { length: number } T must have a numeric length property (strings, arrays, and custom objects all qualify).
T extends object T must be a non-primitive (excludes string, number, boolean, etc.).
K extends keyof T K must be one of the literal property names of T.
T extends SomeClass T must be an instance-compatible shape of SomeClass.
T extends A & B T must satisfy both interfaces A and B at once.

Examples

Example 1: constraining to a shape

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`Length: ${value.length}`);
  return value;
}

logLength("Hello, TypeScript!");
logLength([1, 2, 3, 4]);
logLength({ length: 10, unit: "cm" });

Output:

Length: 18
Length: 4
Length: 10

Strings, arrays, and a custom object all satisfy HasLength because TypeScript’s structural typing only cares that a numeric length property exists — the object doesn’t need to explicitly implement the interface. Without the constraint, value.length would be a compile error because plain T guarantees nothing.

Example 2: keyof constraints for safe property lookup

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

const user = {
  name: "Ada Lovelace",
  age: 36,
  isAdmin: true,
};

const name = getProperty(user, "name");
const age = getProperty(user, "age");

console.log(name, age);

Output:

Ada Lovelace 36

Here two type parameters work together: K extends keyof T restricts K to the literal keys of whatever T turns out to be, and the return type T[K] (an indexed access type) resolves to the exact type of that property. Calling getProperty(user, "name") infers T as the shape of user and K as the literal type "name", so the compiler knows the result is a string — try passing "email" and tsc rejects it immediately, because "email" is not assignable to keyof T.

Example 3: constraining to a reusable domain shape

interface Identifiable {
  id: number;
}

function updateEntity<T extends Identifiable>(
  entities: T[],
  id: number,
  changes: Partial<T>
): T[] {
  return entities.map((entity) =>
    entity.id === id ? { ...entity, ...changes } : entity
  );
}

interface Task extends Identifiable {
  title: string;
  done: boolean;
}

const tasks: Task[] = [
  { id: 1, title: "Write lesson", done: false },
  { id: 2, title: "Review PR", done: false },
];

const updated = updateEntity(tasks, 1, { done: true });
console.log(updated);

Output:

[ { id: 1, title: 'Write lesson', done: true }, { id: 2, title: 'Review PR', done: false } ]

The constraint T extends Identifiable guarantees every entity has an id field to compare against, while still letting the function operate on Task[], or any other array of objects with an id, and return the exact same type it received — not a widened Identifiable[]. This is the core value of generic constraints: gain just enough type information to do the job safely, without throwing away the caller’s specific type.

How it works step by step / Under the hood

  • At each call site, the compiler infers (or checks an explicitly supplied) type argument for every type parameter.
  • For every constrained parameter, it verifies the inferred/supplied type is assignable to the constraint — this is a structural check, the same one used everywhere else in TypeScript (does the argument have all the required members, with compatible types?).
  • If the check fails, tsc reports an error at the call site (not inside the generic function’s body) and compilation stops for that statement.
  • If the check passes, inside the function body the compiler treats the parameter as having the constraint’s type — you get autocomplete and safety for the constraint’s members, but not for members outside the constraint, even if you happen to know a specific caller has them.
  • The return type and any other place T appears are still tracked as the specific inferred T, not the constraint — this is why updateEntity above returns Task[], not Identifiable[].
  • After type checking succeeds, type erasure removes every trace of the constraint, the type parameter, and all annotations. The compiled JavaScript for logLength is just a plain function that reads value.length — there is no runtime check verifying the argument actually has a length. Safety is entirely a compile-time guarantee.

Common Mistakes

Mistake 1: accessing members without a constraint

function printLength<T>(value: T) {
  console.log(value.length);
}

This fails to compile: tsc reports “Property ‘length’ does not exist on type ‘T’.” Because T is unconstrained, the compiler has no guarantee that whatever is passed in has a length property at all — it must reject the access to stay sound.

function printLength<T extends { length: number }>(value: T) {
  console.log(value.length);
}

printLength("TypeScript");
printLength([1, 2, 3]);

Output:

10
3

Adding extends { length: number } gives the compiler the guarantee it needs, and the function still accepts any type that structurally matches — strings, arrays, or custom objects.

Mistake 2: indexing a generic object with a plain string

function getValue<T>(obj: T, key: string) {
  return obj[key];
}

Under --strict, this reports “Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘T’.” A plain string could be any string at all, including ones that don’t exist on T, so TypeScript can’t prove the access is safe.

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

const settings = { theme: "dark", fontSize: 14 };
console.log(getValue(settings, "theme"));

Output:

dark

Constraining K to keyof T restricts the key to the actual property names of T, so the compiler can resolve obj[key] to a precise type and reject invalid keys like "missing" at the call site.

Best Practices

  • Constrain to the smallest interface that describes what your function actually uses — this keeps the function usable with the widest range of argument types (a principle sometimes called “accept the minimal shape you need”).
  • Prefer K extends keyof T over widening a key parameter to string whenever the key must correspond to a real property.
  • Remember that a constraint changes what’s allowed inside the function, not what’s returned — keep returning T (or T[K], Partial<T>, etc.) rather than the constraint type, so callers don’t lose specificity.
  • Combine constraints with intersections (T extends A & B) when a value genuinely needs to satisfy two independent shapes, rather than creating one large interface.
  • Use a default type parameter (<T extends object = {}>) for generic utilities where omitting the type argument should fall back to something sensible rather than erroring.
  • Don’t over-constrain “just in case” — a tighter-than-necessary constraint rejects valid callers for no benefit.

Practice Exercises

  • Write a generic function merge<T extends object, U extends object>(a: T, b: U) that returns a new object combining both, typed as T & U. Verify with tsc that calling it on two object literals infers the correct combined type.
  • Write pluck<T, K extends keyof T>(items: T[], key: K): T[K][] that extracts one property from every item in an array, then call it on an array of objects to collect just their ids.
  • Take the broken printLength snippet from the Common Mistakes section (with no constraint) and, without looking back at the fix, figure out the exact constraint needed to make value.toUpperCase() compile instead of value.length.

Summary

  • Generic constraints use T extends ConstraintType to narrow an otherwise-unbounded type parameter to types that have specific members.
  • The constraint only affects what’s allowed inside the function body — the inferred type argument, and thus the return type, stays as specific as the caller’s actual argument.
  • K extends keyof T is the standard pattern for safely looking up a property whose name is itself a parameter.
  • Constraint checks happen entirely at compile time via structural typing; after checking succeeds, all type information — including the constraint — is erased from the emitted JavaScript.
  • Constrain to the smallest shape your function actually needs, to keep it usable with as many argument types as possible.