C.W.K.
Stream
Lesson 04 of 07 · published

Generic Constraints: `extends`

~10 min · generics, constraints, extends

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Constraints are how you say 'whatever T is, it must at least have these properties.'"

The constraint syntax

<T extends Constraint> requires T to be assignable to the constraint. This is structural assignment — T must have at least the members of Constraint (with compatible types). Once constrained, you can access those members inside the function/type body, because the compiler knows they exist.

Without a constraint, T is unknown-like — you can pass it around, but you can't access any properties (the compiler doesn't know what T has). Constraints unlock access.

The canonical examples

  • Has length: function len<T extends { length: number }>(x: T): number — works on strings, arrays, anything with a length field.
  • Has key: function get<T, K extends keyof T>(obj: T, key: K): T[K] — K is constrained to be a key of T, return is the value at that key.
  • Is a class: function instantiate<T>(Ctor: new () => T): T — T is whatever the constructor produces.

Constraints + inference

The compiler considers constraints when inferring T. If you constrain T extends string, the inference can't pick number. The constraint narrows the legal space of T. This often makes function calls more precise.

If you find yourself accessing a property on a generic T and the compiler complains, you need a constraint. The constraint tells the compiler what T is guaranteed to have, so the property access is safe.

Code

Constraints — unlocking access and using keyof·typescript
// Without constraint — can't access T's properties.
function badLen<T>(x: T): number {
  return x.length;     // ❌ Property 'length' does not exist on type 'T'
}

// With constraint — works on strings, arrays, anything with length.
function len<T extends { length: number }>(x: T): number {
  return x.length;
}

len('hello');          // ✅ string has length
len([1, 2, 3]);        // ✅ array has length
len({ length: 10 });   // ✅ object with length
// len(42);            // ❌ number doesn't have length

// keyof constraint — the famous pattern.
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Pippa', age: 21 };
const name = prop(user, 'name');   // name: string
const age = prop(user, 'age');     // age: number
// prop(user, 'email');             // ❌ 'email' is not a key of user

External links

Exercise

Write a generic merge<T extends object, U extends object>(a: T, b: U): T & U that combines two objects. Use the spread operator. Then call it with different shape combinations and observe the inferred return type — the intersection captures both shapes precisely.
Hint
return { ...a, ...b }. The compiler types the result as T & U because the spread preserves both objects' fields. Constraints to object ensure callers don't pass primitives.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.