Generics Mastery
📖 Concept
Generics are type-level variables — they let you write reusable code that works with different types while maintaining type safety. Think of them as function parameters, but for types.
Syntax: <T> is a type parameter. Convention: T (Type), K (Key), V (Value), E (Element), R (Return).
Generic constraints (<T extends Something>) limit what types can be used — ensuring the generic has certain properties.
Default generics (<T = DefaultType>) provide a fallback type when none is specified.
Key patterns:
- Generic functions:
function identity<T>(value: T): T - Generic interfaces:
interface Box<T> { value: T } - Generic classes:
class Stack<T> { ... } - Generic constraints:
<T extends { length: number }> - Multiple type params:
<K extends keyof T, V extends T[K]>
Generics are the foundation of TypeScript's utility types (Partial<T>, Pick<T, K>, etc.) and are essential for building reusable libraries.
🏠 Real-world analogy: Generics are like a shipping box with a label slot. The box works the same regardless of what's inside — books, electronics, clothes. The label (type parameter) tells you what's in this particular box. The constraint (extends) is like a weight limit: "Box for items under 50 lbs."
💻 Code Example
1// Basic generic function2function identity<T>(value: T): T {3 return value;4}5const str = identity("hello"); // T inferred as string6const num = identity(42); // T inferred as number78// Generic with constraint9function getLength<T extends { length: number }>(item: T): number {10 return item.length;11}12getLength("hello"); // ✅ strings have .length13getLength([1, 2, 3]); // ✅ arrays have .length14// getLength(42); // ❌ number doesn't have .length1516// Multiple type parameters17function map<T, U>(arr: T[], fn: (item: T) => U): U[] {18 return arr.map(fn);19}20const lengths = map(["hello", "world"], s => s.length);21// Type: number[] (T = string, U = number — both inferred!)2223// keyof constraint — type-safe property access24function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {25 return obj[key];26}27const user = { name: "Alice", age: 30, email: "alice@test.com" };28const name = getProperty(user, "name"); // Type: string29const age = getProperty(user, "age"); // Type: number30// getProperty(user, "phone"); // ❌ Error: "phone" not in keyof User3132// Generic interfaces33interface ApiResponse<T> {34 data: T;35 status: number;36 message: string;37 timestamp: Date;38}39const userResponse: ApiResponse<User> = {40 data: { id: 1, name: "Alice", email: "", createdAt: new Date() },41 status: 200,42 message: "Success",43 timestamp: new Date()44};4546// Generic class47class Stack<T> {48 private items: T[] = [];49 push(item: T): void { this.items.push(item); }50 pop(): T | undefined { return this.items.pop(); }51 peek(): T | undefined { return this.items.at(-1); }52 get size(): number { return this.items.length; }53}54const numStack = new Stack<number>();55numStack.push(1);56numStack.push(2);57numStack.pop(); // Type: number | undefined5859// Default type parameter60interface PaginatedResult<T, M = Record<string, unknown>> {61 items: T[];62 total: number;63 page: number;64 meta: M;65}66// M defaults to Record<string, unknown>67const result: PaginatedResult<User> = {68 items: [],69 total: 0,70 page: 1,71 meta: {}72};7374// Generic factory function75function createInstance<T>(Ctor: { new(): T }): T {76 return new Ctor();77}7879// Conditional return type with generics80function wrapInArray<T>(value: T): T extends any[] ? T : T[] {81 return (Array.isArray(value) ? value : [value]) as any;82}
🏋️ Practice Exercise
Mini Exercise:
- Write a generic
first<T>(arr: T[]): T | undefinedfunction - Create a generic
Pair<A, B>interface and use it - Write a type-safe
pluck<T, K extends keyof T>(items: T[], key: K): T[K][] - Build a generic
EventEmitter<Events>where event names and payloads are typed - Create a generic
Result<T, E = Error>type with a default error type
⚠️ Common Mistakes
Using
anyinstead of generics —function first(arr: any[]): anyloses all type information; use<T>(arr: T[]): TinsteadOver-constraining generics —
<T extends string | number>when just<T>would work; only constrain when you need specific operationsNot letting TypeScript infer type arguments —
identity<string>('hello')is redundant;identity('hello')infersstringautomaticallyForgetting that generic type parameters are erased at runtime — you can't do
if (T === string)ornew T()at runtimeCreating too many type parameters — if your function has
<A, B, C, D, E>, it's too complex; simplify the design
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Generics Mastery