Generic Constraints
Restrict type parameters to enable richer operations
In Generics Basics, we saw how unconstrained type parameters like <T> let you write functions that work with any type. But sometimes "any type" is too permissive. What if you need to call a method on T, or access a property, or ensure T is a subtype of something?
This is where generic constraints come in. You can restrict a type parameter to types that satisfy certain conditions using the extends keyword. This unlocks richer operations: accessing properties, calling methods, indexing into objects, and more — all while keeping the code generic and type-safe.
The problem: operations on unconstrained types
Suppose you want a function that logs the length of an array or a string. With an unconstrained type parameter, this doesn't work:
function logLength<T>(item: T): void {
console.log(item.length); // ❌ Error: Property 'length' does not exist on type 'T'
}The compiler doesn't know if T has a length property. For all it knows, T could be number or boolean, which don't have length.
The solution: constrain T to types that do have a length property.
The <T extends Lengthwise> syntax means "T can be any type, as long as it's assignable to Lengthwise." Inside the function, you can safely access item.length because the compiler knows it exists.
'extends' in generics
In the context of generics, extends means "is assignable to" or "is a subtype of." It's the same extends you see in class inheritance (class Dog extends Animal), but here it constrains a type parameter rather than defining a class hierarchy.
The keyof operator
One of the most useful constraints is keyof, which gives you the union of all keys of a type. Combined with extends keyof, you can write functions that safely access object properties.
Here's what's happening:
K extends keyof Tmeans "Kmust be one of the keys ofT."- For
person,keyof Tis"name" | "age" | "city". - So
Kcan only be"name","age", or"city"— anything else is a compile error. - The return type
T[K]is the type of the property at keyK. For"name", it'sstring. For"age", it'snumber.
This is type-safe property access: the compiler guarantees the key exists and gives you the correct property type.
Indexed access types
The syntax T[K] is an indexed access type (also called a lookup type). It looks up the type of property K in type T. This is how you get the precise type of a property dynamically:
The compiler infers K = "name", so T[K] becomes Person["name"], which is string. This is how libraries like Lodash's get or pick functions are typed — they use keyof and indexed access types to preserve precise property types.
Default type parameters
Sometimes you want a type parameter to have a default value if the caller doesn't provide one explicitly. You can specify defaults with <T = DefaultType>.
Default type parameters are useful for APIs where most calls use the same type, but you want to allow overrides. React's useState<T = undefined>() is a classic example: if you don't provide a type, it defaults to undefined.
Preserving precise input types
Sometimes you want to preserve the exact input type, not just its general category. For example, if the input is a readonly array, you want the output to be readonly too. Or if the input is a literal type, you want to keep it as a literal, not widen it to string.
The trick: use extends to constrain the input, but still bind to the precise type.
By constraining T extends readonly unknown[], we accept both mutable and readonly arrays, and the return type T[0] preserves the element type — including literal types like "red".
Precise types unlock better inference
When you preserve precise input types (readonly, literals, exact shapes), the compiler can give you more accurate autocomplete and catch more bugs. This is especially important in library code, where you don't control the input types.
A practical example: typed pluck
Let's build a pluck function that extracts a property from an array of objects, with full type safety:
The pluck function is generic over both the object type (T) and the key type (K). The constraint K extends keyof T ensures the key exists, and the return type T[K][] gives you an array of the property's type.
A practical example: typed pick
Here's another useful utility: pick, which creates a new object with only the specified properties:
The built-in Pick<T, K> utility type creates a new type with only the properties K from T. We use it as the return type to guarantee that the result only has the picked properties.
Utility types recap
TypeScript has many built-in utility types like Pick<T, K>, Omit<T, K>, Partial<T>, Required<T>, etc. They're all defined using generic constraints, mapped types, and conditional types. Understanding constraints is the first step to understanding these utilities.
Variance and assignability (a preview)
When you have generic types, you might wonder: is Box<Dog> assignable to Box<Animal> if Dog extends Animal? The answer depends on variance:
- Covariant: If
A extends B, thenContainer<A>extendsContainer<B>. (Most readonly containers are covariant.) - Contravariant: If
A extends B, thenHandler<B>extendsHandler<A>. (Function parameters are contravariant.) - Invariant: No subtyping relationship. (Mutable containers are often invariant.)
TypeScript infers variance automatically based on how the type parameter is used. You don't usually need to think about it, but it's good to know the terms:
This is an advanced topic, and the compiler handles it automatically. The key takeaway: when you use generics with subtyping, the compiler checks whether the assignment is safe based on how the type parameter is used (in input positions, output positions, or both).
Deep dive in advanced courses
Variance is a deep topic that involves understanding how type parameters flow through function signatures, how they're used in properties, and how the compiler enforces soundness. For now, just know that the compiler is checking these rules behind the scenes. When you see an error like "Type 'A' is not assignable to type 'B'," variance might be the culprit.
Challenge: Generic groupBy
Now it's your turn. You'll implement a generic groupBy function that groups an array of objects by a specified key.
Implement a generic groupBy<T, K extends keyof T> function that:
- Takes an array of objects of type
T. - Takes a key
Kthat exists inT. - Returns a
Record<string, T[]>where each key is a stringified value ofT[K], and each value is an array of objects with that key value.
Hint: Use String(item[key]) to convert the key value to a string for use as a record key.
Multiple choice check
Given this function signature:
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
If you call getValue({ x: 10, y: "hello" }, "y"), what is the return type?
string | number
string
T[K]
When to use generic constraints
Use constraints when:
- You need to access properties or methods on the type parameter.
<T extends Lengthwise>lets you useitem.length. - You're implementing property access utilities.
<K extends keyof T>ensures the key exists. - You want to enforce a minimum structure.
<T extends { id: string }>ensures all inputs have anid. - You need to preserve precise input types.
<T extends readonly unknown[]>accepts both mutable and readonly arrays. - You want a default but allow overrides.
<T = string>defaults tostringif not specified.
Don't over-constrain
Only add constraints when you need them. If your function truly works with any type, leave it unconstrained. Over-constraining makes your API less flexible and harder to use.
Recap
Generic constraints let you write generic code that's more expressive and type-safe. Instead of accepting any type, you can restrict type parameters to types that satisfy certain conditions — like having a length property, being a key of another type, or extending a base type.
Key takeaways:
extendsconstraints:<T extends BaseType>restrictsTto subtypes ofBaseType.keyofoperator:K extends keyof TrestrictsKto keys ofT.- Indexed access types:
T[K]looks up the type of propertyKinT. - Default type parameters:
<T = DefaultType>provides a fallback ifTisn't specified. - Preserving precise types: Use
extendscarefully to keep readonly arrays, literal types, and exact shapes intact. - Variance (preview): Covariant, contravariant, and invariant positions affect how generic types relate to each other under subtyping.
Generic constraints are the key to building rich, reusable abstractions like pluck, pick, groupBy, and the entire ecosystem of TypeScript utility types. Once you master them, you'll write libraries and helpers that feel as polished and type-safe as the standard library itself.
You've now completed the Type-Driven Design and Generics chapters. You've learned how to use type inference, structural typing, discriminated unions, domain modeling, and generic abstractions to write code that's both flexible and safe. The next step is to explore advanced types — conditional types, mapped types, template literal types, and more — but you already have the foundation to write world-class TypeScript.