Skip to main content

Constraints in generics

What a constraint is

A constraint is a condition of the form T extends ... with which you limit the set of allowed substitutions for the type parameter T. The goal is to let you safely access properties/methods inside the function/type body that are guaranteed to exist on T.

javascript
function len<T extends { length: number }>(x: T) { return x.length; // safe: T has length }

If the passed type does not satisfy the constraint, there will be a compile-time error.


Basic forms of constraints

1) Structural (shape/structural)

We constrain the "shape" of the type: the required fields and their types.

javascript
function save<T extends { id: string; name: string }>(entity: T) { /* ... */ }

Plus: works flexibly with "richer" types (a superset of fields is allowed).


2) Constraint by primitives/unions

We allow only the given base types or their union.

javascript
function toStr<T extends string | number | boolean>(x: T): string { return String(x); }

3) "Objects only"

Commonly used variants:

javascript
function f1<T extends object>(x: T) {} // excludes primitives function f2<T extends Record<string, unknown>>(x: T) {}// keys: string, values: unknown function f3<T extends Record<PropertyKey, unknown>>(x: T) {} // allows string|number|symbol keys

object is broad, but excludes null, undefined and primitives. Record specifies the shape of a dictionary more precisely.


4) Dependent parameters: keyof + indexed access

A classic pattern: the key depends on the object, the value depends on the key.

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

5) Arrays/tuples/readonly arrays

javascript
function first<T extends readonly unknown[]>(arr: T) { return arr[0]; // type - T[number] / tuple element } const a = first([1, 2, 3] as const); // 1 | 2 | 3

readonly unknown[] prevents mutation; T extends any[] is a mutable array.


6) Callable/constructable types

We constrain to functions or classes.

javascript
type AnyFn = (...args: any[]) => any; function wrap<T extends AnyFn>(fn: T) { /* ... */ } type Ctor<A extends any[] = any[], R = any> = new (...args: A) => R; function make<T extends Ctor>(C: T): InstanceType<T> { return new C(); }

7) Template literal string types

We constrain to strings of a certain "format".

javascript
type ID = `id_${number}`; function load<T extends ID>(id: T) { /* ... */ } load("id_42"); // fine load("user_42"); // Error

8) Excluding null/undefined

javascript
function ensure<T extends NonNullable<unknown>>(x: T) { // x is neither null nor undefined }

How constraints affect type inference

  1. They refine inference: the compiler looks for the "narrowest" value within the given set.
javascript
function id<T extends string | number>(x: T): T { return x; } const a = id(42); // T = 42 (literal), then narrows to number if needed
  1. They let you link parameters: without K extends keyof T, the compiler cannot guarantee that a key is valid for T.
  2. They improve autocomplete: inside the function body, the methods/fields from the constraint are available.

Important: extends here is a constraint on type parameters. Do not confuse it with extends in conditional types (T extends U ? X : Y), where it is a subtype check used to pick a branch, not a constraint on a parameter.


Combining and techniques

Intersections for a "minimum set of fields"

javascript
type AtLeast<T, K extends keyof T> = Pick<T, K> & Partial<T>; function useUser<U extends AtLeast<{ id: string; name: string; age: number }, "id" | "name">>(u: U) {}

Constraint + default values

javascript
function pair<T, U extends string | number = string>(a: T, b?: U): [T, U] { return [a, b as U]; }

"Only the keys that match a type"

javascript
type KeysOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T]; function pickByType<T extends object, V>(obj: T, key: KeysOfType<T, V>): V { return obj[key] as V; }

Common mistakes and pitfalls

  • Too broad an object: gives no guarantees about values/keys. For dictionaries, Record<PropertyKey, unknown> is better.
  • Forgetting the keyof link: function g<T, K>(obj: T, key: K) leaves the key unbound to T.
  • Too narrow a constraint: breaks inference (TS stops inferring literal types). Widen it to unions/templates.
  • You cannot narrow it inside the body: if T extends { x: number }, you can use x, but you cannot "narrow T to a specific variant" without additional checks/conditional types.

Brief summary

  • Use T extends ... to control the allowed substitutions and get guarantees inside the implementation.
  • Basic patterns: structural shape, unions of primitives, dictionaries (Record), dependent keys (K extends keyof T), arrays/tuples/readonly, functions/constructors, template strings.
  • Constraints improve inference and autocomplete, making generic code genuinely type-safe.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.