Overview

Generics let you write functions, classes, and interfaces that work with multiple types while preserving type information. They are the difference between a function that returns any and one that returns exactly the type you put in.

Without Generics: The Problem

function identity(value: any): any {
  return value;
}

const result = identity("hello"); // result is any

The caller loses all type information. Autocomplete stops working, and type errors go undetected.

With Generics: The Solution

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

const result = identity("hello"); // result is string

T is a type parameter, a placeholder that TypeScript fills in when the function is called.

Generic Constraints

Sometimes you need to guarantee that T has certain properties. Use the extends keyword:

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

getLength("hello");        // OK
getLength([1, 2, 3]);      // OK
getLength(42);             // Error: number does not have length

Generic Interfaces

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

const userResponse: ApiResponse<{ name: string }> = {
  data: { name: "Alice" },
  status: 200,
  message: "OK"
};

Generic Classes

class Stack<T> {
  private items: T[] = [];

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

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

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
const top = numbers.pop(); // number | undefined

Default Type Parameters

function createPair<K, V = string>(key: K, value: V): [K, V] {
  return [key, value];
}

createPair("id", "123");       // [string, string]
createPair("count", 42);       // [string, number]

Common Generic Patterns

PatternSyntaxUse case
Identity functionfunction f<T>(x: T): TPreserve input type
Constraintfunction f<T extends U>(x: T)Require specific properties
Multiple parametersfunction f<K, V>(k: K, v: V)Independent types
Default type<T = string>Optional type argument
Keyof constraint<T, K extends keyof T>Type-safe property access

Real-World Example: Typed API Client

async function get<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(response.statusText);
  return response.json() as Promise<T>;
}

interface User {
  id: number;
  name: string;
}

const user = await get<User>("/api/users/1");
// user is typed as User