TypeScript Generics: A Practical Guide Without the Theory Overload
Introduction
As a developer, I've found that TypeScript generics can be a powerful tool for building reusable and type-safe code. In this article, I'll share my experience with using generics in real-world projects, focusing on when and why to use them.
Basic Generics
Generics allow us to define functions, classes, and interfaces that can work with multiple types, while maintaining type safety. For example, consider a simple function that returns its argument:
function identity(arg: any): any {
return arg;
}
This function works, but it's not type-safe. Using generics, we can rewrite it as:
function identity<T>(arg: T): T {
return arg;
}
Now, the function is type-safe and reusable.
Constraints
Constraints allow us to restrict the types that can be used with a generic. For example:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
In this example, K is constrained to be a key of T, ensuring that we can only access valid properties.
Generic Functions
Generic functions are useful when we need to perform an operation that works with multiple types. For example:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
This function merges two objects into one, preserving their types.
Generic Classes
Generic classes are useful when we need to create a class that can work with multiple types. For example:
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
This class provides a simple way to store and retrieve values of any type.
Real-World Use Cases
TypeScript generics have many real-world use cases, such as:
- Creating reusable utility functions
- Building type-safe data structures
- Implementing algorithms that work with multiple types
Example: Creating a Type-Safe Cache
We can use generics to create a type-safe cache that can store values of any type:
class Cache<T> {
private values: { [key: string]: T };
constructor() {
this.values = {};
}
get(key: string): T | undefined {
return this.values[key];
}
set(key: string, value: T): void {
this.values[key] = value;
}
}
This cache is type-safe and reusable, making it a great example of the power of generics.
Practical Takeaways
- Use generics when you need to create reusable and type-safe code
- Constraints can help restrict the types that can be used with a generic
- Generic functions and classes can be used to perform operations that work with multiple types
- Real-world use cases include creating reusable utility functions, building type-safe data structures, and implementing algorithms that work with multiple types