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".
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
3Both 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
.idis safe to touch. The constraint is the whole contract, andTmight have nothing else, soitem.namewould 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 missingid.
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
Tis declared:<T extends { length: number }>. It is part of declaring the type parameter, not part of the parameter list. - Both a
stringand aboolean[]have a numericlength, so both calls compile. The two calls infer different types forTand 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.