Overview
TypeScript generics enhance flexibility while retaining type safety. This tutorial explores type constraints within generics, ensuring that variables comply with specific contracts.
Introduction to Generics
Generics are a tool that enable the creation of reusable components by providing a means to use types as parameters in classes, interfaces, and functions. This allows for the definition of a function or a component that can work over a variety of types rather than a single one.
function identity<T>(arg: T): T {
return arg;
}
Here, <T>
is a type variable that captures the type the user provides (e.g., number), so that this information can be used later on.
Type Constraints in Action
Type constraints allow you to define requirements for type variables. You can require that a type variable extends a certain type, ensuring it has certain properties.
function loggingIdentity<T extends { length: number }>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
In this example, T
is now constrained to types that have a length
property.
Using Type Parameters in Generic Constraints
You can declare a type parameter that is constrained by another type parameter. These are often used in function operations where one type parameter must match the type of another.
function findKey<K extends keyof T, T>(obj: T, key: K) {
return obj[key];
}
This function ensures that key
is actually a key of obj
.
Default Type Parameters
TypeScript 2.3 introduced default type parameters which allow you to specify default types for generic type parameters.
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
Here, T
is defaulted to string
if no type argument is provided.
Generic Classes
Just like functions, TypeScript also allows for generics in classes. Type constraints can similarly be applied.
class GenericNumber<T extends number> {
zeroValue: T;
add: (x: T, y: T) => T;
}
This class holds a generic property of type T
which is constrained to extend the number
type.
Conditional Types
Conditional types select types based on a condition:
type Check<T> = T extends string ? 'yes' : 'no';
let a: Check<string>; // 'yes'
let b: Check<number>; // 'no'
This is powerful when combined with generics that have constraints.
Using Class Types in Generics
When creating factories in TypeScript using generics, you might also need to use class types:
function create<T>(c: { new(): T }): T {
return new c();
}
This function uses a generic T and defines a constraint expecting a constructor signature for T using the new()
syntax.
Advanced Patterns
As applications grow in complexity, more advanced patterns such as using type constraints to access properties based on a parameter become useful:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
This function is not only type-safe, but it’s also dynamic as per the keys of object obj
.
Utility Types and Type Manipulation
TypeScript provides several utility types to make manipulating types easier. These can be combined with generics and constraints for sophisticated type shaping:
function updateObject<T>(obj: T, prop: keyof T, value: T[keyof T]): T {
return { ...obj, [prop]: value };
}
Here, the utility of keyof T
alongside a type constraint ensures that the updated property exists on the object.
Good Practices
When adding type constraints:
- Ensure they are as permissive as possible to maintain flexibility.
- Avoid unnecessary constraints that can reduce the utility of a generic function or type.
- Inspect the constraints based on actual usage patterns within your codebase.
Conclusion
Type constraints in TypeScript generics provide a powerful tool for maintaining type safety in flexible code. By understanding and utilizing these constraints properly, you can create robust, reusable, and scalable codebases.