Generics let you write reusable, type-safe functions, classes, and types that work over a type parameter (like <T>) rather than a fixed type — preserving the relationship between input and output types.
function identity<T>(x: T): T { return x } returns the same type it receives, so identity('a') is string and identity(5) is number — no any needed. Generics power reusable utilities (a typed API client get<T>(url): Promise<T>, a first<T>(arr: T[]): T | undefined) and containers (class Stack<T>). You can CONSTRAIN a type parameter with extends (<T extends { id: number }>) to require certain members, and provide defaults (<T = string>). Generics keep code DRY without losing type information — the caller's type flows through. Test frameworks use them heavily (e.g. Playwright/Cypress typed responses, typed fixtures).
A typed API helper: async function apiGet<T>(url: string): Promise<T> { return (await fetch(url)).json(); } — const user = await apiGet<User>('/api/user') gives a fully-typed user with autocomplete.
function firstItem(arr: any[]): any loses type info. How do generics fix it, and what does the caller gain?