Course outline · 0% complete

0/26 lessons0%

Course overview →

Constraints: extends for Type Parameters

lesson 6-3 · ~11 min · 20/26

When "any type" is too broad

Inside firstOf<T>, you could not do much with a T: it might be anything, so the compiler allows almost nothing on it. Suppose you want the longer of two values by their .length. Plain <T> fails, a.length is not known to exist.

A constraint narrows which types are allowed to fill the slot:

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

T extends { length: number } means "any type that has a numeric length property". Strings qualify, arrays qualify, { length: 5 } qualifies, numbers do not. Inside the body .length is now safe, and each call still returns the precise input type, not a vague "something with length".

T extends { length: number } accepted string number[] { length: 5 } rejected number { name: string } no numeric length
A constraint sets a minimum requirement. Types carrying the required member pass, whatever else they add.

Strings and arrays both satisfy one constraint

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

console.log(longest("hello", "hi"));
console.log(longest([1, 2, 3], [4, 5]).length);

Output

hello
3

Both calls compile because both string and array have a numeric length, which is the only thing the constraint asks for. The second call chains .length on the result, and that works because the return type is the precise input type number[], not some blurred "thing with a length".

Calling longest(10, 20) is rejected, and the error is worth reading in full: type 'number' does not satisfy the constraint '{ length: number; }'. Numbers have no length property, so they cannot fill the slot.

Constraints on arrays of objects

Constraints are most useful when the elements are objects, because you can require the one property you need and stay open about the rest.

function ids<T extends { id: number }>(items: T[]): number[] {
  const out: number[] = [];
  for (const item of items) {
    out.push(item.id);
  }
  return out;
}

console.log(ids([{ id: 3, name: "pen" }, { id: 7, name: "pad" }]).join(","));
console.log(ids([{ id: 1 }, { id: 2 }]).length);

Output

3,7
2

What the constraint does and does not promise

  • Inside the body, only .id is safe to touch. The constraint is the whole contract, and T might have nothing else, so item.name would be rejected even though the first call happens to pass objects with names.
  • Extra properties are welcome at the call site. { id: 3, name: "pen" } satisfies { id: number } because structural typing from lesson 3-1 asks only that the required members are present.
  • Both calls infer a different T, and each satisfies the constraint independently, so both compile from the same declaration.
  • Calling ids([{ name: "pen" }]) fails with an error about the missing id.

Which argument a constraint rejects

For function pick<T extends { id: number }>(items: T[]): T, the argument that gets rejected is an array of { name: string } objects, because they lack an id.

A constraint is a minimum requirement, not an exact shape. Types carrying id plus any number of extra properties satisfy it, which follows directly from the structural typing of lesson 3-1: having more than required is never a problem.

Only types missing the required id are turned away. So { id: number; name: string } passes, { id: number } passes, and { name: string } does not.

describeLength: a constrained one-liner

describeLength<T extends { length: number }> reports the length of anything that has one.

function describeLength<T extends { length: number }>(value: T): string {
  return "length " + value.length;
}

console.log(describeLength("typescript"));
console.log(describeLength([true, false]));

Output

length 10
length 2

Reading the declaration

  • The constraint goes inside the angle brackets, right where T is declared: <T extends { length: number }>. It is part of declaring the type parameter, not part of the parameter list.
  • Both a string and a boolean[] have a numeric length, so both calls compile. The two calls infer different types for T and neither needed an explicit type argument.
  • "typescript" has 10 characters and the array has 2 elements, which is why the outputs differ. The function never needed to know which kind of thing it received.