Generics let you write code that works over many types while preserving full type safety. Instead of choosing between duplicating a function for every type or falling back to any, you write it once with a type parameter that gets filled in at each call site. Generics power almost every library type you use — Array<T>, Promise<T>, Map<K, V> — so learning to read and write them is a major milestone.
Consider a function that returns the first element of an array:
function firstAny(arr: any[]): any {
return arr[0];
}
const n = firstAny([1, 2, 3]); // n is any — type information lostWith a generic type parameter, the relationship between input and output is preserved:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const a = first([1, 2, 3]); // a: number | undefined
const b = first(["x", "y"]); // b: string | undefinedT is a placeholder. TypeScript infers it from the argument, so you almost never need to write first<number>([1, 2, 3]) explicitly.
Sometimes your function needs the type parameter to have certain capabilities. Constraints express that:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // OK: strings have length
longest([1, 2], [3]); // OK: arrays have length
longest(10, 20); // Error: number has no lengthA particularly useful combination pairs a constraint with keyof to type property access safely:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada" };
pluck(user, "name"); // string
pluck(user, "email"); // Error: "email" is not a key of userTypes themselves can be parameterized. This is how you model reusable containers like API responses:
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
type UserResponse = ApiResponse<{ id: number; name: string }>;
type ListResponse = ApiResponse<string[]>;Defaults are allowed too: interface Box<T = string> { value: T }.
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
get size(): number { return this.items.length; }
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push("two"); // Error: string is not assignable to numberOne Stack implementation, safely reusable for any element type.
Promise<T> is a generic you use daily. Typing a fetch helper makes the whole call chain safe:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as Promise<T>;
}
const todos = await getJson<{ id: number; title: string }[]>("/api/todos");
todos[0].title; // fully typedT, K, V; use descriptive names like TItem or TResponse when there are several.extends so implementations can actually use the values they receive.any destroys.<T> at call sites is rare.extends constrains what a type parameter must support; K extends keyof T types property access.Next lesson: Utility Types — Partial, Pick, Omit, Record, and friends built on generics.