ESC

Type to search the knowledge base.

Generic Constraints

Limit type parameters with extends so generics stay flexible but can safely use properties like id, length, or keyof T.

intermediate4 min read
  • typescript
  • generic-constraints

Unconstrained generics accept everything — which means you can do almost nothing inside the function body. Constraints (T extends …) declare the minimum shape T must have so the implementation can use real properties while callers stay polymorphic.

Docs: Generics — Constraints, keyof.

The problem without constraints

function getId<T>(item: T) {
  // return item.id; // Error: Property 'id' does not exist on type 'T'
}

Constraint with extends

function getId<T extends { id: string }>(item: T): string {
  return item.id;
}

getId({ id: '1', name: 'Ada' }); // ok
// getId({ name: 'Ada' }); // error

T is still a specific type at each call site — you get the full object type in, not a stripped { id: string } only, when you return T:

function withLog<T extends { id: string }>(item: T): T {
  console.log(item.id);
  return item;
}

const u = withLog({ id: '1', role: 'admin' as const });
// u.role is 'admin'

keyof constraints

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

const user = { id: '1', age: 30 };
const age = pluck(user, 'age'); // number
// pluck(user, 'nope'); // error

This is the core of typed form field helpers, translators, and selectors.

Multiple bounds via intersection

type Identified = { id: string };
type Timestamped = { updatedAt: string };

function save<T extends Identified & Timestamped>(entity: T): T {
  return { ...entity, updatedAt: new Date().toISOString() };
}

Or constrain to a union of allowed primitives:

function parseId<T extends string | number>(raw: T): T {
  return raw;
}

Constraining to constructors / instances

function create<T>(Ctor: new () => T): T {
  return new Ctor();
}

class Store {}
const s = create(Store); // Store
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest([1, 2], [1, 2, 3]); // number[]
longest('ab', 'abcd'); // string

Default type parameters + constraints

function createMap<K extends string | number | symbol = string, V = unknown>() {
  return new Map<K, V>();
}

Defaults keep call sites clean; constraints keep misuse loud.

React prop pattern

type ListProps<T extends { id: string }> = {
  items: T[];
  onSelect: (item: T) => void;
};

function List<T extends { id: string }>({ items, onSelect }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <button type="button" onClick={() => onSelect(item)}>
            {item.id}
          </button>
        </li>
      ))}
    </ul>
  );
}

Don’t over-constrain

// Too tight — forces full User when only id needed
function ban<T extends User>(user: T) { /* … */ }

// Better
function ban(user: { id: string }) { /* … */ }
// or
function ban<T extends { id: string }>(user: T): T { /* … */ }

If you never use T in the return type or elsewhere, a concrete parameter type is simpler than a generic.

Constraints vs type guards

Constraints are static. They don’t validate runtime JSON:

function trustMe<T extends { id: string }>(data: T) {
  return data.id;
}
// trustMe(JSON.parse(x)) // still a cast/lie if you force T

Pair API boundaries with runtime validation and unknown.

Footguns

  1. T extends any — disables useful checking; avoid.
  2. T extends object — excludes primitives but allows arrays; be intentional.
  3. Circular constraints — simplify with intermediate types.
  4. Using constraints to “document” without using the property — delete the generic.

Interview out-loud answer

“Constraints limit type parameters so the body can use properties safely while preserving the caller’s concrete type. K extends keyof T is the classic key-safe accessor. I avoid generics when a concrete type would do, and I never treat a constraint as runtime validation.”

Further reading

Related guides