Typescript Generic Constraints: Apply constraints to generics for more precise type definitions.
In TypeScript, generic constraints allow you to enforce certain rules on generic types, making your type definitions more precise and your code safer.
Constraints are specified using the extends
keyword, which restricts what types can be used as generic arguments.
Here are some examples of how to use generic constraints in TypeScript:
1. Basic Constraints with extends
You can constrain a generic type to extend a specific type, ensuring that only types compatible with that constraint are accepted.
// A generic function that only accepts objects with a `name` propertyfunction printName<T extends { name: string }>(obj: T): void {console.log(obj.name);}printName({ name: "Alice" }); // ✅ WorksprintName({ name: "Bob", age: 25 }); // ✅ Works// printName({ age: 25 }); // ❌ Error: Property 'name' is missing
In this example, T
is constrained to types that have a name
property of type string
.
2. Constraining to a Union of Types
You can restrict a generic to a specific set of types by using union types in the constraint.
function getLength<T extends string | any[]>(input: T): number {return input.length;}getLength("hello"); // ✅ Works, returns 5getLength([1, 2, 3]); // ✅ Works, returns 3// getLength(123); // ❌ Error: Type 'number' is not assignable to type 'string | any[]'
Here, T
can only be a string
or an array (any[]
), both of which have a length
property.
3. Constraining to Class Instances
You can use classes as constraints to ensure the generic type is an instance of a particular class.
class Animal {sound() {console.log("Some sound");}}class Dog extends Animal {bark() {console.log("Woof!");}}function makeSound<T extends Animal>(animal: T): void {animal.sound();}makeSound(new Dog()); // ✅ Works// makeSound({} as string); // ❌ Error: Type 'string' is not assignable to type 'Animal'
In this example, T
is constrained to be of type Animal
or any subclass of Animal
, so you can call sound()
on animal
.
4. Multiple Constraints with &
You can apply multiple constraints by combining them with &
. This requires the generic type to satisfy all specified constraints.
interface Identifiable {id: number;}interface Nameable {name: string;}function printPersonInfo<T extends Identifiable & Nameable>(person: T): void {console.log(`ID: ${person.id}, Name: ${person.name}`);}printPersonInfo({ id: 1, name: "Alice" }); // ✅ Works// printPersonInfo({ id: 1 }); // ❌ Error: Property 'name' is missing// printPersonInfo({ name: "Alice" }); // ❌ Error: Property 'id' is missing
Here, T
is constrained to types that have both id
and name
properties.
5. Using keyof to Constrain to Keys of a Type
You can use keyof
to constrain a generic type to be one of the keys of another type.
type Person = {id: number;name: string;age: number;};function getProperty<T extends keyof Person>(key: T): string {return `Property name: ${key}`;}getProperty("name"); // ✅ WorksgetProperty("age"); // ✅ Works// getProperty("address"); // ❌ Error: Type '"address"' is not assignable to parameter of type 'keyof Person'
Here, T
is constrained to be one of the keys of the Person
type (id
, name
, or age
), so you can't pass a key that doesn't exist on Person
.
6. Default Values with Constraints
You can set a default type with a constraint to avoid having to specify the type in every call.
function createEntity<T extends { id: number } = { id: number }>(entity: T): T {console.log(entity.id);return entity;}createEntity({ id: 1, name: "Alice" }); // ✅ Works with inferred typecreateEntity<{ id: number; age: number }>({ id: 2, age: 30 }); // ✅ Works with explicit type
In this example, if no type argument is provided, T
defaults to { id: number }
.