Constraining a generic with extends
A bare <T> means "any type at all" — which is often too permissive. If your generic function needs
to use a property of T (its length, its id), a bare T will not let you, because T might be a
type that lacks it. Constraints — T extends SomeType — restrict a type parameter to types that
have what you need, and they are what make generics both flexible and safe.
The problem: you cannot use what T might not have
function longest<T>(a: T, b: T): T {
return a.length > b.length ? a : b; // error: Property 'length' does not exist on type 'T'
}
This fails because T could be anything — a number, a boolean — and those have no .length.
TypeScript is right to stop you: it cannot guarantee T has a length. A bare type parameter only
lets you do things valid for every possible type, which is almost nothing (assign it, pass it, put it
in an array). To use .length, you must promise that T has a length — and that promise is a
constraint.
The solution: extends
T extends Constraint restricts T to types that are assignable to the constraint — types that have
at least what the constraint requires:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length > b.length ? a : b; // fine now — T is guaranteed to have a length
}
longest("Pune", "Mumbai"); // fine — strings have length
longest([1, 2], [1, 2, 3]); // fine — arrays have length
longest(10, 20); // error: number does not have a length property
T extends { length: number } says "T can be any type, as long as it has a length: number
property". Now inside the function you may use a.length, because every T is guaranteed to have one.
And the constraint is checked at the call site: longest(10, 20) fails because a number does not
satisfy the constraint. A constraint is a promise about T that unlocks the corresponding
capabilities inside the function and is enforced on callers.
Crucially, the return type is still the specific T you passed — longest("Pune", "Mumbai") returns
a string, not { length: number }. You constrain T to require a length, but callers still get
back their exact type. That is the win over just typing the parameters as { length: number }: you
keep the precise type while requiring the capability.
The keyof constraint — a type-safe property getter
The most useful constraint in everyday code relates two type parameters, using keyof (which you will
meet fully in the advanced-types module). It types a function that safely gets a property by name:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const member = { name: "Kavita", age: 33, city: "Pune" };
getProperty(member, "name"); // returns string (T[K] where K is "name")
getProperty(member, "age"); // returns number
getProperty(member, "email"); // error: Argument '"email"' is not assignable to '"name" | "age" | "city"'
Read the two type parameters: T is the object's type, and K extends keyof T means "K is one of
T's keys". keyof T for the member above is the union "name" | "age" | "city", so K is
constrained to those keys — passing "email" is a compile error because it is not a key of member.
And the return type T[K] (an indexed access type) is "the type of the property at key K" — so
getProperty(member, "age") returns exactly number. This is a genuinely useful, fully type-safe
property accessor, and it is a beautiful demonstration of constraints: two related type parameters, one
constraining the other, with a precise return type. You will see this pattern in real libraries.
Constraining to a shape with an id, and to a class
Constraints are how you write generics that work with "any type that has these properties":
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id); // fine — every T has an id
}
const users = [{ id: "1", name: "Kavita" }, { id: "2", name: "Ravi" }];
const found = findById(users, "1"); // returns { id: string; name: string } | undefined — the FULL type
T extends HasId requires every T to have an id: string, so item.id is safe inside — while
findById(users, "1") still returns the full user type (with name), not just HasId. This "works
with anything that has an id" pattern is everywhere in real code — repositories, caches, data stores —
and it is the everyday face of constraints.
Constraints with defaults, and multiple constraints
A type parameter can be both constrained and have a default:
function makeStore<T extends { id: string } = { id: string }>() { /* ... */ }
And you constrain to multiple requirements with an intersection:
function process<T extends { id: string } & { name: string }>(item: T) {
console.log(item.id, item.name); // both guaranteed
}
T extends { id: string } & { name: string } requires T to have both an id and a name. Use an
intersection in the constraint when a T must satisfy several shapes at once.
The mental model
A constraint answers the question "what does this generic need to know about T?" A bare <T> knows
nothing about T, so it can do almost nothing with it — only pass it around. The moment you want to
use T — access a property, call a method, index into it — you must constrain T to guarantee that
capability. So: write the loosest constraint that lets your function do its job. No constraint if
you only pass T around (like identity or first); extends { length: number } if you need a
length; extends keyof T for a key; extends HasId for an id. The constraint documents the generic's
requirements and keeps it as flexible as possible while staying safe.
Check your work
Why a bare <T> is often too permissive. T could be any type, so you can only do what is valid
for every type — you cannot use T-specific properties like .length.
What T extends Constraint does. Restricts T to types assignable to the constraint (having at
least what it requires), unlocking those capabilities inside and enforcing them on callers.
Whether the return type is widened by the constraint. No — callers still get their exact T back;
you require a capability without losing precision.
The keyof constraint. K extends keyof T restricts K to T's keys, and T[K] (indexed
access) returns the precise property type — a type-safe property getter.
The HasId pattern. T extends HasId lets a generic work with "any type that has an id" while
returning the full type — common in repositories and stores.
Constraint plus default, and multiple constraints. A type parameter can be constrained and have a
default; use an intersection (A & B) to require several shapes.
The mental model. Write the loosest constraint that lets the function do its job — none if you only
pass T around, a specific one when you need to use T.
Practice
- Write
longest<T>(a, b)using.lengthon a bareTand read the error. Addextends { length: number }and confirm it works for strings and arrays but not numbers. - Confirm
longest("Pune", "Mumbai")returnsstring, not{ length: number }. - Write
getProperty<T, K extends keyof T>(obj, key): T[K]. Get a valid property (confirm the precise return type) and an invalid key (read the error). - Write
findById<T extends HasId>and confirm it returns the full item type. - Add a default to a constrained type parameter and use it both ways.
- Constrain a
Tto two shapes with an intersection and use a property from each. - For three generic functions, decide the loosest constraint each needs and justify it.
Official documentation
- TypeScript — Generic Constraints —
extendson a type parameter. - TypeScript — Using Type Parameters in Generic Constraints — The
keyof/getPropertypattern. - TypeScript — keyof and indexed access — Covered fully in the advanced-types module.
Next: generic interfaces and classes.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship