Generic constraints and defaults

TypeScript · Practical · 12 min

Constraints let generic code use properties of T safely; without them generics can do almost nothing.

An unconstrained T could be anything, so inside the function you can't touch any of its properties. A constraint says what T must at least be:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("abc", "de");   // T = string
longest([1, 2], [3]);   // T = number[]
longest(10, 20);        // error: number has no length

Note that the function returns T, not { length: number } — callers keep their precise type. That's the advantage over simply typing the parameter as { length: number }.

Constraints can refer to other type parameters. The classic case is a typed property getter:

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

keyof T is the union of T's property names, so K must be a real key, and T[K] is the type of that property (both operators are covered in depth later). prop(user, "nmae") is a compile error and prop(user, "age") returns number.

Defaults work like default parameters: interface Box<T = string> lets callers write Box alone. A default must satisfy the constraint if there is one: <T extends object = {}>.

A subtle point: a constraint is a lower bound on knowledge, not the type itself. Inside function f<T extends string>(x: T) you can't return "hello" as T, because T might be the narrower literal "bye". When you find yourself needing as T, the signature usually needs rethinking.

When inference picks a type from the "wrong" argument, NoInfer<T> (TS 5.4) marks a position that should not be used for inference.

Example

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

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

interface Paginated<T = string> { items: T[]; next?: string }

function pickColor<C extends string>(colors: C[], fallback: NoInfer<C>): C {
  return colors[0] ?? fallback;
}

const user = { name: "Ada", age: 36 };
const longer = longest("abc", "de");        // string
const age = prop(user, "age");              // number
const page: Paginated = { items: ["a"] };   // T defaults to string
const c = pickColor(["red", "blue"], "red"); // "red" | "blue"

// @ts-expect-error -- "nmae" is not assignable to "name" | "age"
prop(user, "nmae");
// @ts-expect-error -- "green" not in "red" | "blue" thanks to NoInfer
pickColor(["red", "blue"], "green");
console.log(longer, age, page.items, c);

Remember

  • T extends X lets you use X's members on T while keeping T precise
  • K extends keyof T + T[K] type property access safely
  • Defaults (<T = string>) make generics easier to use
  • NoInfer<T> stops a parameter from influencing inference

Watch out for

  • Returning a concrete value as T — the caller's T may be narrower than the constraint.

Check yourself

  1. Why is function f<T extends { id: number }>(x: T): T better than function g(x: { id: number }): { id: number }?
  2. prop(obj, key) is declared <T, K extends keyof T>(obj: T, key: K): T[K]. For obj = { a: 1, b: "x" } and key "b", what is returned?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints