Right, so you’ve got generics. They’re fantastic. You slap a T on a function and suddenly it works for anything. Freedom! Power! Until you try to T.toLowerCase() and the whole thing explodes because, newsflash, not every type T has a toLowerCase method. TypeScript, in its infinite wisdom, doesn’t assume your T is a string; it assumes your T is an enigma wrapped in a mystery. This is where we stop being polite and start getting real with extends.

The extends keyword in a type parameter is your way of putting up a fence. It’s not a suggestion; it’s a requirement. You’re telling TypeScript, “Look, I know T can be anything, but for this to work, it had better be at least this kind of thing.” You’re constraining the wild possibilities of T to a more manageable, useful subset. It’s the difference between “bring anyone to the party” and “bring anyone who knows how to mix a decent Old Fashioned.”

The Basic Syntax: Building a Fence

Here’s how you build that fence. You declare your generic type parameter and immediately tell it what it needs to extend.

function getLength<T extends { length: number }>(thing: T): number {
  return thing.length;
}

Let’s read this function signature together. It says: “I am a function named getLength. I work with any type T, but T must have a property called length that is a number. I will then return that number.”

This is now perfectly valid:

getLength("hello"); // TypeScript knows: T = string, string has .length -> OK
getLength([1, 2, 3]); // T = number[], arrays have .length -> OK
getLength({ length: 42, name: "The Answer" }); // T = { length: number }, has .length -> OK

And this is gloriously, correctly invalid:

getLength(42); // Compiler Error: Type 'number' has no property 'length'

You’ve successfully communicated your requirements to the type system. You’ve moved from a runtime error (thing.length is not a function) to a compile-time error, which is a massive upgrade. You’ve just turned a potential bug into a conversation.

Why Not Just Use a Specific Type?

You might look at that first example and think, “Why not just write (thing: { length: number }): number and skip the generic T altogether?” It’s a fair question. The crucial difference is in what the function returns.

In the constrained generic version, we’re not just returning number; we’re returning information about the specific type we were given. Watch this:

// Without Generic Constraint
function getLengthBasic(thing: { length: number }): number {
  return thing.length;
}

const basicResult = getLengthBasic("hello"); // basicResult is typed as 'number'

// With Generic Constraint
function getLengthAdvanced<T extends { length: number }>(thing: T): T {
  // Let's do something absurd to prove a point
  console.log(`I am definitely returning a ${typeof thing}`);
  return thing; // We return the actual thing, not just its length
}

const advancedResult = getLengthAdvanced("hello"); // advancedResult is typed as 'string'

The constraint ensures the input is valid, but by using T, we preserve all the specific type information. The first function tells you the length. The second function tells you the length and knows it’s dealing with a string, which is far more powerful if you need to do anything else with the returned value.

Using Interfaces and Type Aliases as Constraints

You don’t have to write the constraint inline. In fact, for anything non-trivial, you absolutely shouldn’t. Use an interface or a type alias. It’s cleaner and reusable.

interface HasEmail {
  email: string;
  name: string;
}

function sendEmail<T extends HasEmail>(recipient: T): void {
  // TypeScript now knows for a fact that `recipient` has .email and .name
  console.log(`Sending email to ${recipient.name} at ${recipient.email}`);
}

const user = { name: "Alice", email: "alice@example.com", age: 30 };
sendEmail(user); // Works perfectly! T is { name: string; email: string; age: number }

const incompleteUser = { name: "Bob" };
sendEmail(incompleteUser); // Compiler Error: Property 'email' is missing.

This is the pattern you’ll see everywhere in serious TypeScript code. The constraint (HasEmail) defines the minimum required shape, while the generic T can be any shape that meets or exceeds that minimum. The function sendEmail doesn’t care if T also has an age, a phoneNumber, or a favoriteColor property; it only requires that it has the properties defined in HasEmail.

The Key Pitfall: It’s a Minimum, Not an Exact Match

This is the most common mental hurdle. extends does not mean “is exactly this.” It means “is at least this.” The constrained type T must have all the properties of the constraint, but it can (and often will) have more. Your function must be written to handle that gracefully. You can only safely access the properties guaranteed by the constraint. Accessing other properties will cause a type error because those properties aren’t guaranteed to exist for every possible T.

function tryToAccessAge<T extends HasEmail>(person: T): void {
  console.log(person.email); // OK, guaranteed by HasEmail
  console.log(person.age);   // Compiler Error: Property 'age' does not exist on type 'T'
}

The compiler is protecting you from yourself. You told it T must have email. You did not tell it T must have age. So it rightly stops you. If your function logic genuinely requires a specific property like age, that property must be included in the constraint.