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.
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:
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.
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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
Key Insights
- Use
extendsto 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
keyofto safely access object properties in generic functions. - Constraints only narrow what types are allowed. They do not change how the generic works at runtime.
Frequently Asked Questions
Can I use a union type as a constraint?
What happens when I use a constraint that the inferred type does not satisfy?
Can a type parameter constrain another type parameter?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.