Generic Constraints & Defaults

An unconstrained type parameter can be anything, so you can't use any of its properties. A constraint — T extends Something — says "any type, as long as it has at least this shape", which unlocks those properties while keeping the function generic.

Why constraints are needed

TS no-constraint.ts
function longest<T>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
tsc output
error TS2339: Property 'length' does not exist on type 'T'.

TypeScript is right: T could be a number, and numbers don't have a length.

Adding a constraint with extends

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

console.log(longest("apple", "fig"));
console.log(longest([1, 2], [1, 2, 3]));
longest(10, 20);
tsc output
error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Strings and arrays both have a numeric length, so they're accepted, and the return type stays specific — calling with strings returns a string, not just "something with a length". Numbers are rejected at the call site. Remove the last line and the program prints apple and [ 1, 2, 3 ].

Constraining to an interface

TS interface-constraint.ts
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const products = [
  { id: 1, name: "Lamp", price: 30 },
  { id: 2, name: "Desk", price: 120 },
];

const desk = findById(products, 2);
console.log(desk?.name, desk?.price);
Output
Desk 120

Inside findById we can only rely on id. But because the return type is T, the caller gets back the full product type, including name and price.

Using one type parameter in another's constraint

Constraints can refer to other type parameters. A classic example is safely reading a property by name, using keyof (covered properly in lesson 24):

TS keyof-constraint.ts
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Ida", age: 28 };
const age = getProp(user, "age");
console.log(age + 1);

getProp(user, "email");
tsc output
error TS2345: Argument of type '"email"' is not assignable to parameter of type '"name" | "age"'.

K extends keyof T means "K must be one of T's property names", and the return type T[K] is the type of that specific property — number for "age".

Default type parameters

Like default function arguments, a type parameter can have a default that's used when it isn't specified or inferred:

TS defaults.ts
interface Box<T = string> {
  value: T;
}

const a: Box = { value: "text" };
const b: Box<number> = { value: 42 };

class EventBus<Payload = void> {
  private handlers: ((p: Payload) => void)[] = [];
  on(fn: (p: Payload) => void) { this.handlers.push(fn); }
  emit(p: Payload) { this.handlers.forEach(h => h(p)); }
}

const ticks = new EventBus<number>();
ticks.on(n => console.log("tick", n));
ticks.emit(1);
console.log(a.value, b.value);
Output
tick 1
text 42

Defaults must come after any type parameters without defaults, and they can be combined with constraints: <T extends object = {}>.

const type parameters

Normally, an array or object literal passed to a generic is widened (["a", "b"] becomes string[]). Adding const before a type parameter asks TypeScript to infer the narrowest literal type, as if the caller had written as const:

TS const-param.ts
function routes<const T extends readonly string[]>(paths: T): T {
  return paths;
}

const r = routes(["/home", "/about"]);
type Route = (typeof r)[number];

const go = (to: Route) => console.log(`navigating to ${to}`);
go("/about");
Output
navigating to /about

Here Route is "/home" | "/about" rather than string, so go("/contact") would be an error. This feature arrived in TypeScript 5.0.

Try it yourself

Try it yourself
Console output