In TypeScript, generic constraints let you set extra requirements on the type parameters used in generic functions or classes. These constraints mean the type parameter must meet specific conditions to be used in the generic context. In this topic, you'll learn about what generic constraints are and how to use them in your projects.
What are generic constraints
When using generic functions or classes, you might need to limit the capabilities of the allowed types. Imagine we have a function that logs the length of the passed argument, but we need to make sure the argument used with the function has a length property. If it doesn't, it will cause an error:
function logLength<T>(arg: T): void {
console.log(arg.length);
// Compilation error: Property 'length' does not exist on type 'T'.
}If the given argument has at least the length property, the error disappears. To address this issue, you can employ generic constraints.
Generic constraints enforce specific requirements on the types used in a generic function, assuring type safety and protecting against likely errors. Let's look at how to use them.
Using generic constraints
In the previous example, we had to restrict the arguments to have a length property. To do so, we create a new interface that outlines our constraint:
interface LengthConstraint {
length: number;
}
function logLength<T extends LengthConstraint>(arg: T): void {
console.log(arg.length);
}To apply a constraint to a generic type parameter, you use the extends keyword followed by a type or a mix of types. This detail signifies that the generic type must be a subtype of the defined constraint.
In this example, we set up an interface LengthConstraint with a single property length of the type number. The logLength function receives a generic type T that extends LengthConstraint. It ensures any argument passed to the function has a length property of type number. Hence, you can safely access the length property within the function body.
You can call the logLength function with different types that fulfill the LengthConstraint constraint, such as strings, arrays, or objects with a length property:
logLength("Hello"); // Output: 5
logLength([1, 2, 3]); // Output: 3
logLength({ length: 10, value: "test" }); // Output: 10However, if you attempt to call it with a type that lacks a length property, TypeScript will send a compilation error:
logLength(3);
// Compilation error: Argument of type 'number' is not assignable to parameter of type 'LengthConstraint'.Generic constraints provide a way to include more specific type data in your generic code, enabling you to create more reusable and type-safe functions and classes.
Constrain a type parameter by another
In TypeScript, you can declare a type parameter that is constrained by another type parameter. This means the values permitted for the first type parameter will depend on the value of the second type parameter.
To declare a type parameter restrained by another type parameter, you use the previously mentioned extends keyword followed by the name of the reliant type parameter. This ensures the first type parameter is compatible with, or extends, the type specified by the second type parameter.
Here's an example to illustrate this idea:
function getKeyValue<Type, Key extends keyof Type>(obj: Type, key: Key): [Key, Type[Key]] {
return [key, obj[key]];
}
let exampleObj = { a: 1, b: 2, c: 3, d: 4 };
console.log(getKeyValue(exampleObj, "a")); // Output: ["a", 1]
console.log(getKeyValue(exampleObj, "e")); // Compilation error: Argument of type '"e"' is not assignable to parameter of type '"a" | "b" | "c" | "d"' In this example, we declare the getKeyValue function that takes two parameters: obj of type Type and key of type Key. The restriction Key extends keyof Type ensures that the key parameter is a valid key of the Type object. The keyof Type operator returns a union of all the keys of the Type object, and the constraint restricts Key to be one of those keys. If we try to call this function with an invalid key, TypeScript will send us a compilation error, preventing likely runtime errors.
Using type parameters in generic constraints with the keyof operator allows you to ensure that the property key passed to the function is valid for the given object type.
Conclusion
To conclude, generic constraints in TypeScript let you enforce relationships and limitations among multiple type parameters in generic functions or classes. By using the extends keyword, you can specify a type parameter must extend or be assignable to a specific type or mix of types.
We examined several examples to display the use of generic constraints. From ensuring an array has a length property to validating property keys for object access, generic constraints offer type safety and help avoid potential runtime errors by enforcing specific requirements on the types used in generic contexts.