Last 30 Days
No notifications
Generics let you write code that works with *any* type while still keeping that type relationship intact. Instead of any (which discards type info) or one-off copies, a generic captures a type at the call site and threads it through your function, class, or interface. They're how Array, Promise, Map, and every serious React component are typed.
// Bad — loses type info
function firstAny(arr: any[]): any { return arr[0]; }const x = firstAny([1, 2, 3]); // x: any — no autocomplete, no safety
A generic captures the element type and returns it back:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
const u = first<User>(users); // User | undefined (explicit)
T is a *type parameter* — a placeholder that TS fills in for each call.
Conventional single-letter names (carry-overs from Java/C++): T (generic), K (key), V (value), E (element), R (return). For complex code, use full words: TItem, TError. Anything is allowed — pick clarity.
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}const p = pair("age", 21); // [string, number]
interface Box<T> {
value: T;
}const numBox: Box<number> = { value: 1 };
type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: string };
const r: ApiResponse<User> = { ok: true, data: { id: 1, name: "Ada" } as User };
class Stack<T> {
private items: T[] = [];
push(x: T) { this.items.push(x); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
get size() { return this.items.length; }
}const s = new Stack<number>();
s.push(1); s.push(2);
const top = s.pop(); // number | undefined
extendsSometimes T shouldn't be *anything*. Use extends to require a shape:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}longest("hello", "hi"); // string
longest([1, 2, 3], [1]); // number[]
// longest(10, 20); // ❌ number has no .length
keyof + generic — type-safe property accessfunction get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}const user = { name: "Ada", age: 36 };
const n = get(user, "name"); // string
const a = get(user, "age"); // number
// get(user, "email"); // ❌ "email" not a key of user
That's the pattern Lodash's _.pick, Zustand selectors, and form libraries lean on.
interface ApiResponse<T = unknown> {
ok: boolean;
data: T;
}const r: ApiResponse = { ok: true, data: { anything: 1 } };
Useful when callers usually don't care, but power users can override.
type Mapper<T, U> = (x: T) => U;const toLen: Mapper<string, number> = (s) => s.length;
You'll go deeper later, but the pattern starts here:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // falsefunction merge<A extends object, B extends object>(a: A, b: B): A & B {
return { ...a, ...b };
}const m = merge({ id: 1 }, { name: "Ada" }); // { id: number; name: string }
T should appear in at least two positions (input AND output, or two inputs that must match).function load<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : fallback;
} catch {
return fallback;
}
}const settings = load("settings", { theme: "light" }); // { theme: string }
const score = load<number>("score", 0); // number
> Note the as T — JSON.parse returns any. In production you'd validate with Zod and the cast goes away.