Mastering TypeScript Generics: Advanced Patterns and Real-World Use Cases
Learn how to use TypeScript generics to write flexible, reusable code with advanced patterns and real-world examples.
TypeScript generics allow you to create reusable components that work with a variety of types instead of a single one. This makes your functions, classes, and interfaces more flexible and type-safe. In this tutorial, we will explore advanced generics patterns along with real-world use cases that are beginner-friendly.
### What are Generics? Generics provide a way to tell TypeScript what type a value will be while writing code that works with multiple types. Think of it like a placeholder that gets filled when you use the component.
function identity<T>(arg: T): T {
return arg;
}
const output1 = identity<string>("hello"); // output1 is string
const output2 = identity<number>(123); // output2 is numberThe `
### Advanced Generic Patterns
#### 1. Generic Constraints Sometimes you want to restrict the kinds of types that can be passed to a generic. Use `extends` keyword to constrain the type parameter.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now TypeScript knows 'arg' has a length property
return arg;
}
loggingIdentity([1, 2, 3]); // OK
loggingIdentity('hello'); // OK
// loggingIdentity(10); // Error: number doesn't have a length property#### 2. Using Multiple Generic Parameters You can define more than one generic type to connect the relationships between your inputs and outputs.
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const mergedObj = merge({ name: 'Alice' }, { age: 30 });
console.log(mergedObj.name); // Alice
console.log(mergedObj.age); // 30Here, the function merges two objects, preserving the types of both.
#### 3. Generic Interfaces and Classes
Generics aren't limited to functions. Use them to build flexible interfaces and classes.
interface KeyValue<K, V> {
key: K;
value: V;
}
const kv1: KeyValue<number, string> = { key: 1, value: 'apple' };
class DataHolder<T> {
private data: T[] = [];
add(item: T) {
this.data.push(item);
}
getItems(): T[] {
return this.data;
}
}
const numberHolder = new DataHolder<number>();
numberHolder.add(10);
numberHolder.add(20);
console.log(numberHolder.getItems());### Real-World Use Cases
#### 1. Creating a Generic API Response Handler You can use generics to define the shape of data you expect from an API, improving type safety when working with fetch or axios.
interface ApiResponse<T> {
status: number;
payload: T;
}
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url)
.then(response => response.json())
.then(data => ({ status: 200, payload: data }));
}
interface User {
id: number;
name: string;
}
fetchData<User>('/api/user/1').then(response => {
console.log(response.payload.name); // TypeScript knows payload is User
});#### 2. Strongly Typed State Management Generics help you create scalable and type-safe state management solutions.
type StateListener<T> = (state: T) => void;
class StateManager<T> {
private state: T;
private listeners: StateListener<T>[] = [];
constructor(initialState: T) {
this.state = initialState;
}
subscribe(listener: StateListener<T>) {
this.listeners.push(listener);
}
setState(newState: T) {
this.state = newState;
this.listeners.forEach(listener => listener(newState));
}
getState(): T {
return this.state;
}
}
const numberState = new StateManager<number>(0);
numberState.subscribe(state => console.log('Number state:', state));
numberState.setState(42);### Summary Generics empower TypeScript developers to write flexible, reusable, and type-safe code. By mastering constraints, multiple type parameters, and generic interfaces/classes, you can build scalable and maintainable applications. Experiment with these patterns in your projects to improve your coding efficiency and type safety.