Generic Constraints in TypeScript

Generic constraints limit which types a type parameter can accept. Learn how to use the extends keyword to require specific properties while keeping your functions generic.

6 min read

TypeScript generic constraints limit which types a type parameter can accept. Without a constraint, a type parameter stands for every possible type, so you can only use operations that work on all types. A constraint narrows that down so you can safely use specific properties or methods inside the function.

You write a constraint with the extends keyword after the type parameter:

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

The constraint T extends { length: number } means T can be any type, as long as it has a length property that is a number. Inside the function, TypeScript knows that length exists and is safe to access.

typescripttypescript
getLength("hello");
getLength([1, 2, 3]);
getLength({ length: 10, name: "item" });

All three calls work because strings, arrays, and this object literal all have a length property. Now try calling the same function with a value that does not have that property, a plain number:

typescripttypescript
getLength(42);

Numbers have no length property, so TypeScript rejects the call before the code ever runs. The compiler reports the exact reason as an error:

texttext
Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

TypeScript catches the mistake at compile time, before the function ever runs against real data.

Why Constraints Matter

Constraints exist because an unconstrained type parameter could be anything. Without one, TypeScript refuses to let you use properties on a generic value, since it cannot guarantee that every possible type has that property:

typescripttypescript
function logLength<T>(value: T): void {
  console.log(value.length);
}

Without a constraint, TypeScript has no way to know that every possible T has a length property, so it rejects the access with this error:

texttext
Property 'length' does not exist on type 'T'.

TypeScript is right. The type parameter could be a number, a boolean, or anything else without a length property. The constraint tells TypeScript exactly which properties are guaranteed to exist.

Using Interfaces as Constraints

You can define the constraint as a named interface for clarity:

typescripttypescript
interface HasName {
  name: string;
}
 
function greet<T extends HasName>(entity: T): string {
  return `Hello, ${entity.name}!`;
}
 
greet({ name: "Ada", age: 30 });
greet({ name: "Buddy", breed: "Golden Retriever" });

Both objects pass the constraint because each has a name property that is a string. Any extra properties are fine. The constraint defines the minimum shape, not the exact shape.

Constraining with Primitive Types

You can constrain a type parameter to a specific primitive type or union:

typescripttypescript
function double<T extends number | bigint>(value: T): T {
  return value;
}

This is rarely useful on its own because you could just write the parameter as number | bigint. The constraint form is only valuable when the type parameter needs to flow through to the return type or when combined with other type parameters.

Using One Type Parameter to Constrain Another

The most powerful pattern is constraining one type parameter with another:

typescripttypescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { id: 1, name: "Ada", email: "ada@example.com" };
 
const name = getProperty(user, "name");
const id = getProperty(user, "id");

K extends keyof T means: the key parameter must be a valid property name of the object type. Inside the function, accessing obj[key] is always safe because TypeScript knows K is a real property name.

typescripttypescript
getProperty(user, "age");

The key "age" is not one of the object's property names, so the constraint rejects it before the function body ever runs:

texttext
Argument of type '"age"' is not assignable to parameter of type '"email" | "id" | "name"'.

The return type uses an indexed access type that extracts the property value type. Calling with "name" returns a string, and calling with "id" returns a number. For more on this, see indexed access types in TypeScript.

Constraining to a Constructor

You can constrain a type parameter to a class constructor:

typescripttypescript
function createInstance<T extends new () => any>(ctor: T): InstanceType<T> {
  return new ctor();
}

The constraint T extends new () => any means T must be a constructor function that takes no arguments. The built-in InstanceType utility extracts the instance type that the constructor produces. This pattern is useful for factories and dependency injection.

Constraints Do Not Change Runtime Behavior

A constraint is purely a compile-time check. The emitted JavaScript looks identical to an unconstrained generic function:

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

The TypeScript compiler strips the constraint entirely and emits plain JavaScript, with no trace of the type parameter or the extends keyword anywhere in the output:

javascriptjavascript
function getLength(value) {
  return value.length;
}

The constraint's only job is to tell the compiler which properties are safe to use and which call sites are invalid. It never runs at runtime.

Common Mistakes

Constraining to a type that includes the property you need but also many you do not:

typescripttypescript
function process<T extends string>(value: T): string {
  return value.toUpperCase();
}

If you know the argument is always a string, just write value: string. The type parameter here adds no value: it does not let the return type track a more specific type than string, so it only adds noise. Use a constraint only when the function must work with multiple types that share a capability.

Using a constraint so narrow that only one type satisfies it:

typescripttypescript
function parse<T extends { parse: (input: string) => T }>(parser: T, input: string): T {
  return parser.parse(input);
}

If only one or two types can ever satisfy the constraint, you are not really writing a generic function. Consider using a concrete type or a union instead.

Forgetting that the constraint only guarantees the minimum shape:

typescripttypescript
function clone<T extends { name: string }>(obj: T): T {
  return { name: obj.name };
}

This fails because the return value is { name: string }, not T. The constraint says T has at least a name property, but it might have other properties too. Use a spread to preserve them:

typescripttypescript
function clone<T extends { name: string }>(obj: T): T {
  return { ...obj };
}

For more on how constraints work alongside generic interfaces, see that article.

Rune AI

Rune AI

Key Insights

  • Use extends to constrain a type parameter to types that satisfy a specific shape.
  • A constraint lets the function body use properties that exist on the constraint type.
  • TypeScript rejects arguments whose type does not satisfy the constraint at the call site.
  • Combine constraints with keyof to safely access object properties in generic functions.
  • Constraints only narrow what types are allowed. They do not change how the generic works at runtime.
RunePowered by Rune AI

Frequently Asked Questions

Can I use a union type as a constraint?

Yes. Writing `<T extends string | number>` means T must be assignable to `string | number`. But this is usually not what you want. The constraint only ensures T is one of those types. It does not let you use methods from both string and number inside the function without narrowing.

What happens when I use a constraint that the inferred type does not satisfy?

TypeScript reports a compiler error at the call site. The error message says the argument type is not assignable to the constrained type. This is the point of constraints: they catch mismatches early.

Can a type parameter constrain another type parameter?

Yes. This is a common pattern: `<T, K extends keyof T>`. It says K must be a key of T. This guarantees that `obj[key]` is safe inside the function body.

Conclusion

Generic constraints let you say 'this type parameter can be any type, as long as it has these specific properties.' The extends keyword defines the minimum shape a type must satisfy. Constraints keep your functions generic enough to be reusable but specific enough to be useful inside the function body. When you combine constraints with keyof, you can write functions that safely access object properties without losing type information.