> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Generics Deep Dive

> Generic Functions, Classes, Constraints, and Real-World Patterns

<img src="https://mintcdn.com/devweeekends/CHfRzoAmD5TGW2ch/images/courses/typescript-crash-course/generics.svg?fit=max&auto=format&n=CHfRzoAmD5TGW2ch&q=85&s=4401f5c4b90f66372c3226f70ebd75e9" alt="TypeScript Generics" width="1080" height="1080" data-path="images/courses/typescript-crash-course/generics.svg" />

# Generics Deep Dive

Generics are the cornerstone of reusable, type-safe code in TypeScript. They allow you to write code that works with any type while maintaining full type information. Think of generics as "fill-in-the-blank" types -- you write a function or class with placeholder slots (`T`, `U`), and the caller fills in the blanks with concrete types when they use it. It is the same concept as a template in a word processor: the structure is fixed, but the content is customizable.

***

## 1. Why Generics?

Consider this function:

```typescript theme={null}
// Without generics - loses type information
function identity(value: any): any {
  return value;
}

const result = identity('hello'); // type is 'any' - not helpful!
```

With generics:

```typescript theme={null}
// With generics - type is preserved
function identity<T>(value: T): T {
  return value;
}

const result = identity('hello'); // type is 'string'
const num = identity(42);          // type is 'number'
```

***

## 2. Generic Functions

### Basic Syntax

```typescript theme={null}
// Type parameter T
function wrap<T>(value: T): T[] {
  return [value];
}

wrap('hello'); // string[]
wrap(42);      // number[]
wrap(true);    // boolean[]

// Explicit type argument
wrap<string>('hello'); // string[]
```

### Multiple Type Parameters

```typescript theme={null}
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

pair('hello', 42);           // [string, number]
pair(true, { name: 'Alice' }); // [boolean, { name: string }]

// Map-like function
function mapPair<T, U, V>(pair: [T, U], fn: (a: T, b: U) => V): V {
  return fn(pair[0], pair[1]);
}

mapPair([1, 2], (a, b) => a + b); // 3 (number)
```

### Generic Arrow Functions

```typescript theme={null}
// Arrow function with generics
const identity = <T>(value: T): T => value;

// In TSX files, add a trailing comma to avoid JSX ambiguity
const identity2 = <T,>(value: T): T => value;

// With constraint
const getLength = <T extends { length: number }>(item: T): number => {
  return item.length;
};
```

***

## 3. Generic Constraints

Restrict what types can be used with generics. Unconstrained generics accept *anything*, but sometimes your function needs to access specific properties (like `.length` or `.id`). Constraints let you say "T can be any type, but it must have at least these properties." Think of it as a job requirement: "any applicant is welcome, but they must have a driver's license."

### extends Keyword

```typescript theme={null}
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(item: T): number {
  console.log(item.length);
  return item.length;
}

logLength('hello');        // 5 - string has length
logLength([1, 2, 3]);      // 3 - array has length
logLength({ length: 10 }); // 10 - object with length
// logLength(42);          // ❌ Error: number doesn't have length
```

### keyof Constraint

```typescript theme={null}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 25 };

getProperty(user, 'name'); // string
getProperty(user, 'age');  // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key
```

### Multiple Constraints

```typescript theme={null}
interface Printable {
  print(): void;
}

interface Loggable {
  log(): void;
}

function process<T extends Printable & Loggable>(item: T): void {
  item.print();
  item.log();
}
```

### Conditional Constraints

```typescript theme={null}
type NumberOrString<T> = T extends number ? number : string;

function process<T extends number | string>(value: T): NumberOrString<T> {
  if (typeof value === 'number') {
    return (value * 2) as NumberOrString<T>;
  }
  return value.toUpperCase() as NumberOrString<T>;
}

const num = process(42);       // number (84)
const str = process('hello'); // string ('HELLO')
```

***

## 4. Generic Classes

Classes that work with multiple types. Generic classes are the backbone of reusable data structures and service layers in TypeScript. Instead of writing `NumberStack`, `StringStack`, and `UserStack` as separate classes, you write `Stack<T>` once and let the consumer specify the type.

```typescript theme={null}
// Container<T> works with any type. The caller fills in T when creating an instance.
class Container<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}

const stringContainer = new Container('hello');
console.log(stringContainer.getValue()); // 'hello' (string)

const numberContainer = new Container(42);
console.log(numberContainer.getValue()); // 42 (number)
```

### Generic Stack

```typescript theme={null}
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  size(): number {
    return this.items.length;
  }

  clear(): void {
    this.items = [];
  }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop()); // 3
```

### Generic with Static Members

```typescript theme={null}
class Registry<T> {
  private static instances = new Map<string, any>();
  private items = new Map<string, T>();

  register(key: string, item: T): void {
    this.items.set(key, item);
  }

  get(key: string): T | undefined {
    return this.items.get(key);
  }

  static getInstance<U>(name: string): Registry<U> {
    if (!this.instances.has(name)) {
      this.instances.set(name, new Registry<U>());
    }
    return this.instances.get(name);
  }
}
```

***

## 5. Generic Interfaces

```typescript theme={null}
// Generic interface
interface Repository<T> {
  find(id: number): T | undefined;
  findAll(): T[];
  create(item: Omit<T, 'id'>): T;
  update(id: number, item: Partial<T>): T | undefined;
  delete(id: number): boolean;
}

// Generic with multiple parameters
interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

// Extending generic interface
interface TimestampedRepository<T> extends Repository<T> {
  findByDate(start: Date, end: Date): T[];
}
```

### Implementing Generic Interfaces

```typescript theme={null}
interface Entity {
  id: number;
}

interface User extends Entity {
  name: string;
  email: string;
}

class UserRepository implements Repository<User> {
  private users: User[] = [];
  private nextId = 1;

  find(id: number): User | undefined {
    return this.users.find(u => u.id === id);
  }

  findAll(): User[] {
    return [...this.users];
  }

  create(item: Omit<User, 'id'>): User {
    const user = { ...item, id: this.nextId++ };
    this.users.push(user);
    return user;
  }

  update(id: number, item: Partial<User>): User | undefined {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) return undefined;
    this.users[index] = { ...this.users[index], ...item };
    return this.users[index];
  }

  delete(id: number): boolean {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) return false;
    this.users.splice(index, 1);
    return true;
  }
}
```

***

## 6. Default Type Parameters

Provide defaults for generic parameters. Just like default function parameters, you can give type parameters a fallback value. This makes generics more ergonomic -- callers who do not care about the specific type can skip the angle brackets entirely.

```typescript theme={null}
// Default type parameter -- if no T is provided, it defaults to 'any'
// This means Response can be used as Response<User> or just Response
interface Response<T = any> {
  data: T;
  status: number;
  message: string;
}

const response1: Response = { data: 'anything', status: 200, message: 'OK' };
const response2: Response<User> = { data: user, status: 200, message: 'OK' };

// Multiple defaults
interface Paginated<T = any, M = Record<string, unknown>> {
  items: T[];
  total: number;
  page: number;
  metadata: M;
}

// Defaults with constraints
interface Container<T extends object = object> {
  value: T;
}
```

***

## 7. Generic Type Aliases

```typescript theme={null}
// Generic type alias -- reusable type "templates" that any code can instantiate
type Nullable<T> = T | null | undefined;

type MaybeString = Nullable<string>; // string | null | undefined

// This pattern is extremely common in React applications for managing async state.
// Instead of writing separate loading/error/data types for every entity, define it once.
type AsyncResult<T> = {
  loading: boolean;
  error: Error | null;
  data: T | null;
};

type UserResult = AsyncResult<User>;    // { loading: boolean; error: Error | null; data: User | null }
type ProductResult = AsyncResult<Product>; // Same shape, different data type

// Recursive generic
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

// Generic function type
type Mapper<T, U> = (item: T) => U;

const toUpper: Mapper<string, string> = (s) => s.toUpperCase();
const toLength: Mapper<string, number> = (s) => s.length;
```

***

## 8. Generic Utility Patterns

### Factory Pattern

```typescript theme={null}
interface Constructor<T> {
  new (...args: any[]): T;
}

function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
  return new ctor(...args);
}

class User {
  constructor(public name: string) {}
}

const user = createInstance(User, 'Alice'); // User
```

### Builder Pattern

```typescript theme={null}
class QueryBuilder<T> {
  private query: Partial<T> = {};

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.query[key] = value;
    return this;
  }

  build(): Partial<T> {
    return { ...this.query };
  }
}

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

const query = new QueryBuilder<User>()
  .where('role', 'admin')
  .where('name', 'Alice')
  .build();
// { role: 'admin', name: 'Alice' }
```

### Result/Either Pattern

This is one of the most impactful patterns in TypeScript. Instead of throwing exceptions (which bypass the type system), you return a union that forces the caller to handle both success and failure. It makes error handling explicit, composable, and type-safe.

```typescript theme={null}
// The Result type is a discriminated union: 'ok' is the tag field.
// Default error type is Error, but callers can customize it.
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: 'Division by zero' };
  }
  return { ok: true, value: a / b };
}

const result = divide(10, 2);
if (result.ok) {
  // TypeScript narrows: result is { ok: true; value: number }
  console.log(result.value); // 5
} else {
  // TypeScript narrows: result is { ok: false; error: string }
  console.error(result.error);
}
// The caller CANNOT access result.value without checking result.ok first.
// This eliminates "forgot to handle the error" bugs at compile time.
```

### Option/Maybe Pattern

```typescript theme={null}
class Option<T> {
  private constructor(private value: T | null) {}

  static some<T>(value: T): Option<T> {
    return new Option(value);
  }

  static none<T>(): Option<T> {
    return new Option<T>(null);
  }

  map<U>(fn: (value: T) => U): Option<U> {
    if (this.value === null) return Option.none();
    return Option.some(fn(this.value));
  }

  flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
    if (this.value === null) return Option.none();
    return fn(this.value);
  }

  getOrElse(defaultValue: T): T {
    return this.value ?? defaultValue;
  }

  isSome(): boolean {
    return this.value !== null;
  }
}

function findUser(id: number): Option<User> {
  const user = users.find(u => u.id === id);
  return user ? Option.some(user) : Option.none();
}

const userName = findUser(1)
  .map(user => user.name)
  .getOrElse('Unknown');
```

***

## 9. Advanced Generic Patterns

### Variadic Tuple Types

```typescript theme={null}
// Concatenate tuples
type Concat<T extends any[], U extends any[]> = [...T, ...U];

type A = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]

// Function composition
function pipe<A extends any[], B, C>(
  fn1: (...args: A) => B,
  fn2: (arg: B) => C
): (...args: A) => C {
  return (...args) => fn2(fn1(...args));
}

const addOne = (x: number) => x + 1;
const double = (x: number) => x * 2;
const addOneThenDouble = pipe(addOne, double);

console.log(addOneThenDouble(5)); // 12
```

### Recursive Generics

```typescript theme={null}
// Flatten nested arrays
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type Nested = number[][][];
type Flat = Flatten<Nested>; // number

// Deep object paths
type PathKeys<T, Prefix extends string = ''> = T extends object
  ? {
      [K in keyof T]: K extends string
        ? Prefix extends ''
          ? K | `${K}.${PathKeys<T[K], K>}`
          : `${Prefix}.${K}` | `${Prefix}.${K}.${PathKeys<T[K], K>}`
        : never;
    }[keyof T]
  : never;

interface User {
  name: string;
  address: {
    city: string;
    country: {
      code: string;
    };
  };
}

type UserPaths = PathKeys<User>;
// 'name' | 'address' | 'address.city' | 'address.country' | 'address.country.code'
```

### Conditional Generics

```typescript theme={null}
// Different return types based on input
type ArrayOrSingle<T, Multi extends boolean> = Multi extends true
  ? T[]
  : T;

function fetch<T, Multi extends boolean = false>(
  url: string,
  options?: { multi?: Multi }
): Promise<ArrayOrSingle<T, Multi>> {
  // Implementation
  return {} as any;
}

const single = await fetch<User>('/user/1');         // User
const multiple = await fetch<User>('/users', { multi: true }); // User[]
```

***

## 10. Real-World Examples

### Type-Safe Event Emitter

```typescript theme={null}
type EventMap = Record<string, any>;

type EventKey<T extends EventMap> = string & keyof T;
type EventCallback<T> = (data: T) => void;

class EventEmitter<T extends EventMap> {
  private listeners = new Map<keyof T, Set<EventCallback<any>>>();

  on<K extends EventKey<T>>(event: K, callback: EventCallback<T[K]>): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);

    // Return unsubscribe function
    return () => this.off(event, callback);
  }

  off<K extends EventKey<T>>(event: K, callback: EventCallback<T[K]>): void {
    this.listeners.get(event)?.delete(callback);
  }

  emit<K extends EventKey<T>>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach((callback) => callback(data));
  }
}

// Usage
interface AppEvents {
  login: { userId: string };
  logout: { timestamp: Date };
  error: { message: string; code: number };
}

const events = new EventEmitter<AppEvents>();

const unsubscribe = events.on('login', (data) => {
  console.log(`User ${data.userId} logged in`);
});

events.emit('login', { userId: '123' }); // Type-safe!
unsubscribe(); // Clean up
```

### Type-Safe HTTP Client

```typescript theme={null}
interface ApiEndpoints {
  '/users': {
    GET: { response: User[] };
    POST: { body: Omit<User, 'id'>; response: User };
  };
  '/users/:id': {
    GET: { response: User };
    PUT: { body: Partial<User>; response: User };
    DELETE: { response: void };
  };
}

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';

type PathParams<P extends string> = P extends `${string}:${infer Param}/${infer Rest}`
  ? { [K in Param | keyof PathParams<Rest>]: string }
  : P extends `${string}:${infer Param}`
  ? { [K in Param]: string }
  : {};

class HttpClient<T extends Record<string, Record<Method, any>>> {
  async request<
    P extends keyof T & string,
    M extends keyof T[P] & Method
  >(
    method: M,
    path: P,
    options?: {
      params?: PathParams<P>;
      body?: T[P][M] extends { body: infer B } ? B : never;
    }
  ): Promise<T[P][M] extends { response: infer R } ? R : never> {
    // Implementation
    return {} as any;
  }
}

const client = new HttpClient<ApiEndpoints>();

// Fully type-safe API calls
const users = await client.request('GET', '/users');
const newUser = await client.request('POST', '/users', {
  body: { name: 'Alice', email: 'alice@example.com' }
});
```

***

## Summary

| Concept             | Example                                   |
| :------------------ | :---------------------------------------- |
| Basic Generic       | `function identity<T>(val: T): T`         |
| Multiple Parameters | `function pair<T, U>(a: T, b: U): [T, U]` |
| Constraints         | `<T extends { length: number }>`          |
| keyof Constraint    | `<K extends keyof T>`                     |
| Generic Class       | `class Stack<T> { items: T[] }`           |
| Generic Interface   | `interface Repo<T> { find(id): T }`       |
| Default Parameter   | `interface Response<T = any>`             |
| Conditional         | `T extends U ? X : Y`                     |
| infer               | `T extends Array<infer U> ? U : T`        |
| Variadic Tuples     | `[...T, ...U]`                            |

Next, we'll explore modules and configuration!

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Q: A junior developer asks you 'why can't I just use any instead of generics?' Walk me through the argument you would make.">
    **Strong Answer:**

    This is one of the most important questions to get right because it reveals whether you understand the core value proposition of TypeScript's type system.

    * **`any` destroys type information**: `function identity(value: any): any` accepts anything and returns `any`. The caller gets no information back. `const result = identity('hello')` -- `result` is `any`, so TypeScript cannot tell you it is a string. You can call `result.nonExistentMethod()` and TypeScript will not warn you.
    * **Generics preserve type information**: `function identity<T>(value: T): T` -- when you call `identity('hello')`, TypeScript infers `T = string`, and `result` is `string`. Autocomplete works, method calls are checked, and type errors are caught.
    * **Real-world example**: A `useState` hook. `const [count, setCount] = useState(0)` -- with generics, `count` is `number` and `setCount` only accepts `number`. With `any`, both are unchecked. The generic version preserves the relationship between the state value and the setter.
    * **The compile-time cost argument**: You write a generic function once and every caller benefits from type safety automatically. With `any`, you push the burden to every call site -- every caller must remember the correct types and cast manually.

    **Follow-up: Are there cases where 'any' is actually the right choice over generics?**

    Rarely, but yes. When you are writing a low-level utility that genuinely operates on any value without caring about its type -- like a deep clone function using `JSON.parse(JSON.stringify(x))`. The input and output types are unrelated, so a generic `T` would be misleading. Also during incremental migration from JavaScript, `any` is a pragmatic stepping stone. But in application code, if you reach for `any`, it is almost always a sign that you should be using a generic, a union, or `unknown`.
  </Accordion>

  <Accordion title="Q: Explain the difference between 'T extends SomeType' as a constraint versus as a conditional type. They look similar but do completely different things.">
    **Strong Answer:**

    This is a subtle but critical distinction because the `extends` keyword is overloaded in TypeScript.

    * **As a constraint (in angle brackets)**: `<T extends { length: number }>` means "T must satisfy this shape." It restricts which types can be used as `T`. If you call the function with `number`, the compiler rejects it. The constraint narrows the set of valid type arguments. It is a prerequisite, not a condition.
    * **As a conditional (in ternary)**: `T extends string ? X : Y` means "check at the type level whether T is assignable to string." It does not restrict `T` -- it branches. Both branches produce valid output. It is pattern matching.
    * **Where confusion happens**: `function process<T extends string | number>(value: T): T extends string ? string : number`. Here, `extends` appears twice with different meanings. The first constrains `T` to `string | number`. The second conditionally selects the return type based on what `T` actually is.
    * **Practical example**: `<T extends Printable>` is like a function parameter type -- it says what you can pass in. `T extends Printable ? ... : ...` is like an `if` statement -- it checks what was passed and branches accordingly.

    **Follow-up: Can you use conditional return types with overloads, and which is better?**

    Yes, they solve the same problem. Conditional return types are more concise and work with generics. Overloads are more explicit with separate signatures. My preference is conditional types for 2 variants and overloads for 3+ variants, because conditional types with many branches become hard to read. The key difference is that conditional return types work with generic inference, while overloads require exact signature matching.
  </Accordion>

  <Accordion title="Q: Walk me through the Result/Either pattern in TypeScript generics. Why is it better than throwing exceptions for expected errors?">
    **Strong Answer:**

    The Result pattern is one of the most impactful patterns you can adopt in a TypeScript codebase.

    * **The type**: `type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }`. A discriminated union where `ok` is the tag.
    * **Why it is better than exceptions**: Exceptions are invisible in TypeScript's type system. `function parseJson(input: string): Config` -- the type signature says nothing about the fact that it can fail. With `function parseJson(input: string): Result<Config, ParseError>`, the return type explicitly communicates failure possibility. The compiler forces you to check `result.ok` before accessing `result.value`.
    * **Composability**: Results compose with `map` and `flatMap` operations. This leads to flatter, more readable code than nested try/catch blocks.
    * **When to use exceptions instead**: For truly unexpected errors -- programmer mistakes, invariant violations, out-of-memory. The rule: if the caller is expected to handle the error as part of normal business logic (validation failure, record not found), use `Result`. If the error indicates a bug, throw.
    * **Real-world adoption**: This pattern is standard in Rust, Go (multiple return values), and increasingly in TypeScript. Libraries like `neverthrow` provide full implementations.

    **Follow-up: How does the Result pattern interact with async/await?**

    The function returns `Promise<Result<T, E>>` instead of `Promise<T>`. The caller does `const result = await fetchUser(id); if (!result.ok) return result;` -- early return propagates the error. Libraries like `neverthrow` provide `ResultAsync<T, E>` which wraps Promise and Result together, enabling `.map().mapErr()` chaining without intermediate awaits.
  </Accordion>

  <Accordion title="Q: How would you build a type-safe HTTP client using generics where the route, method, request body, and response type are all statically checked?">
    **Strong Answer:**

    This is one of the most impressive TypeScript patterns because it combines multiple advanced features into a practical API.

    * **Step 1: Define the API contract as a type**: Each route is a key, each HTTP method is a nested key, and each method defines `body` and `response` types. This is the single source of truth for your API shape.
    * **Step 2: Generic request method**: `async request<P extends keyof Endpoints, M extends keyof Endpoints[P]>(method: M, path: P, options?: ...): Promise<Endpoints[P][M]['response']>`. TypeScript constrains `P` to valid paths and `M` to valid methods for that path.
    * **Step 3: Path parameter extraction**: Using template literal types to extract `:id` from `/users/:id` and require `{ id: string }` in the options.
    * **What the caller sees**: `client.request('GET', '/users')` returns `Promise<User[]>`. `client.request('POST', '/users', { body: { name: 'Alice' } })` -- TypeScript checks the body. Passing an invalid route or method is a compile error.

    This pattern is used in production by libraries like `zodios` and internal API clients at companies like Stripe.

    **Follow-up: What is the limitation of this approach, and how would you handle API versioning?**

    The main limitation is that the type definition must be maintained in sync with the actual API. If the backend changes and the TypeScript definition is not updated, the types lie. The solution is code generation: tools like OpenAPI generators produce TypeScript types directly from the API schema. For API versioning, define separate endpoint maps per version and create version-specific client instances.
  </Accordion>
</AccordionGroup>
