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.

typescript
function identity<T>(arg: T): T {
  return arg;
}

const output1 = identity<string>("hello"); // output1 is string
const output2 = identity<number>(123); // output2 is number

The `` syntax defines a generic type parameter. When you call the function, you specify which type `T` should be, or TypeScript can infer it.

### 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.

typescript
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.

typescript
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);  // 30

Here, 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.

typescript
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.

typescript
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.

typescript
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.