Dataslope logoDataslope

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.

Code Block
TypeScript 5.7

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.

Code Block
TypeScript 5.7

Here's what's happening:

  • K extends keyof T means "K must be one of the keys of T."
  • For person, keyof T is "name" | "age" | "city".
  • So K can only be "name", "age", or "city" — anything else is a compile error.
  • The return type T[K] is the type of the property at key K. For "name", it's string. For "age", it's number.

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:

Code Block
TypeScript 5.7

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>.

Code Block
TypeScript 5.7

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.

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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, then Container<A> extends Container<B>. (Most readonly containers are covariant.)
  • Contravariant: If A extends B, then Handler<B> extends Handler<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:

Code Block
TypeScript 5.7

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.

Challenge
TypeScript 5.7
Generic groupBy

Implement a generic groupBy<T, K extends keyof T> function that:

  • Takes an array of objects of type T.
  • Takes a key K that exists in T.
  • Returns a Record<string, T[]> where each key is a stringified value of T[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

QuestionSelect one

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:

  1. You need to access properties or methods on the type parameter. <T extends Lengthwise> lets you use item.length.
  2. You're implementing property access utilities. <K extends keyof T> ensures the key exists.
  3. You want to enforce a minimum structure. <T extends { id: string }> ensures all inputs have an id.
  4. You need to preserve precise input types. <T extends readonly unknown[]> accepts both mutable and readonly arrays.
  5. You want a default but allow overrides. <T = string> defaults to string if 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:

  • extends constraints: <T extends BaseType> restricts T to subtypes of BaseType.
  • keyof operator: K extends keyof T restricts K to keys of T.
  • Indexed access types: T[K] looks up the type of property K in T.
  • Default type parameters: <T = DefaultType> provides a fallback if T isn't specified.
  • Preserving precise types: Use extends carefully 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.

On this page