TypeScript Generics: A Practical Guide Without the Theory Overload
Introduction
As a developer, I've found TypeScript generics to be a powerful tool for creating reusable and type-safe code. In this article, I'll provide a practical guide to using generics in TypeScript, focusing on when and why to use them.
Basic Generics
Generics allow us to create functions and classes that can work with multiple types. We can define a generic type using the <> syntax. For example:
function identity<T>(arg: T): T {
return arg;
}
In this example, T is a generic type that can be any type. We can call the identity function with a string or a number, and it will return the same type:
console.log(identity('hello')); // returns 'hello'
console.log(identity(42)); // returns 42
Constraints
We can also add constraints to our generic types. For example, we can create a function that only works with objects:
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. This means that we can only call getProperty with a key that exists on the object.
Generic Functions
Generic functions are useful when we need to perform an operation that works with multiple types. For example, we can create a map function that works with arrays of any type:
function map<T, U>(arr: T[], callback: (arg: T) => U): U[] {
return arr.map(callback);
}
We can use this map function to transform an array of strings to an array of numbers:
const strings = ['1', '2', '3'];
const numbers = map(strings, (str) => parseInt(str));
console.log(numbers); // returns [1, 2, 3]
Generic Classes
Generic classes are useful when we need to create a class that can work with multiple types. For example, we can create a Container class that can hold any type:
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
We can use this Container class to create a container that holds a string or a number:
const stringContainer = new Container('hello');
console.log(stringContainer.getValue()); // returns 'hello'
const numberContainer = new Container(42);
console.log(numberContainer.getValue()); // returns 42
Real-World Use Cases
TypeScript generics have many real-world use cases. For example, we can use them to create a Cache class that can cache values of any type:
class Cache<T> {
private cache: { [key: string]: T } = {};
get(key: string): T | undefined {
return this.cache[key];
}
set(key: string, value: T): void {
this.cache[key] = value;
}
}
We can use this Cache class to cache strings or numbers:
const stringCache = new Cache<string>();
stringCache.set('key', 'value');
console.log(stringCache.get('key')); // returns 'value'
const numberCache = new Cache<number>();
numberCache.set('key', 42);
console.log(numberCache.get('key')); // returns 42
Practical Takeaways
- Use generics when you need to create reusable code that works with multiple types.
- Use constraints to limit the types that can be used with your generics.
- Use generic functions and classes to create reusable and type-safe code.
- Use real-world use cases, such as caching or mapping, to demonstrate the power of generics.