Generics let you write functions, classes, and types that work with any type while preserving type safety — the type parameter <T> acts as a variable at the type level, inferred from usage or constrained with extends to require specific shapes.
Type variables (<T>) capture types from call sites — TypeScript infers them from arguments, preserving type information through function calls.
extends limits what types a generic accepts — T extends { id: number } requires an id property, K extends keyof T constrains to valid property names.
TypeScript infers generic arguments from usage in most cases — explicit type arguments are only needed when inference fails (empty arrays, ambiguous calls).
Type parameters can have defaults (T = string) — callers can omit the type argument and get the default, similar to default function parameters.
Generics are TypeScript's mechanism for creating reusable, type-safe abstractions. Without generics, you'd need to write separate functions for every type or use any and lose type safety.
Consider a function that returns the first element of an array:
function first(arr: any[]): any { return arr[0]; }This works but loses type information — first([1, 2, 3]) returns any instead of number. You'd need to cast the result or write separate firstNumber, firstString functions.
With generics:
function first<T>(arr: T[]): T { return arr[0]; }
const n = first([1, 2, 3]); // n: number (inferred)
const s = first(['a', 'b']); // s: string (inferred)T is a type parameter — a variable that captures the type from the call site. TypeScript infers T from the argument, so you rarely need to specify it explicitly.
Type parameters are declared in angle brackets before the parameter list:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}Multiple type parameters (T, U) capture different types in the same function. TypeScript infers all of them from the arguments.
Generics work on interfaces and types:
interface ApiResponse<T> { data: T; status: number; error?: string; }
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };Default type parameters (E = Error) provide fallback types when the caller doesn't specify them.
The extends keyword constrains what types a generic can accept:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}K extends keyof T means K must be one of T's property names. This enables precise autocomplete and return types — getProperty(user, 'name') returns the specific type of user.name.
Common constraint patterns:
T extends object — must be an object typeT extends { id: number } — must have a numeric id propertyT extends (...args: any[]) => any — must be a functionT extends readonly any[] — must be an array or tupleTypeScript infers generic type arguments from usage, so explicit specification is rarely needed:
const result = map([1, 2, 3], n => n.toString()); // inferred: map<number, string>Explicit type arguments are needed when inference fails or when you want a more specific type: useState<string[]>([]) because TypeScript would infer never[] from an empty array.
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const stack = new Stack<number>();The type parameter applies to all methods and properties in the class.
<T>(x: T): T — preserves the exact type through a transformation<T>(ctor: new () => T): T — creates instances from constructors<T, K extends keyof T>(obj: T, key: K): T[K] — type-safe property accessMap<K, V>, Set<T>, WeakMap<K, V> — parameterized containersGenerics are type-level variables that capture and preserve type information through transformations. Type parameters are inferred from arguments — explicit specification is rarely needed. Constraints (extends) limit what types are accepted and enable type-safe property access patterns. Use generics when the same logic applies to multiple types and you need to preserve the relationship between input and output types.
Fun Fact
The single-letter convention for generic type parameters (T, U, K, V) comes from Java and C#, which inherited it from academic type theory where T stands for 'Type.' Modern TypeScript style guides increasingly prefer descriptive names (TItem, TResult, TKey) for readability, especially when a function has three or more type parameters.