Generics Mastery

0/5 in this phase0/21 across the roadmap

📖 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

codeTap to expand ⛶
1// Basic generic function
2function identity<T>(value: T): T {
3 return value;
4}
5const str = identity("hello"); // T inferred as string
6const num = identity(42); // T inferred as number
7
8// Generic with constraint
9function getLength<T extends { length: number }>(item: T): number {
10 return item.length;
11}
12getLength("hello"); // ✅ strings have .length
13getLength([1, 2, 3]); // ✅ arrays have .length
14// getLength(42); // ❌ number doesn't have .length
15
16// Multiple type parameters
17function 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!)
22
23// keyof constraint — type-safe property access
24function 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: string
29const age = getProperty(user, "age"); // Type: number
30// getProperty(user, "phone"); // ❌ Error: "phone" not in keyof User
31
32// Generic interfaces
33interface 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};
45
46// Generic class
47class 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 | undefined
58
59// Default type parameter
60interface 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};
73
74// Generic factory function
75function createInstance<T>(Ctor: { new(): T }): T {
76 return new Ctor();
77}
78
79// Conditional return type with generics
80function wrapInArray<T>(value: T): T extends any[] ? T : T[] {
81 return (Array.isArray(value) ? value : [value]) as any;
82}

🏋️ Practice Exercise

Mini Exercise:

  1. Write a generic first<T>(arr: T[]): T | undefined function
  2. Create a generic Pair<A, B> interface and use it
  3. Write a type-safe pluck<T, K extends keyof T>(items: T[], key: K): T[K][]
  4. Build a generic EventEmitter<Events> where event names and payloads are typed
  5. Create a generic Result<T, E = Error> type with a default error type

⚠️ Common Mistakes

  • Using any instead of generics — function first(arr: any[]): any loses all type information; use <T>(arr: T[]): T instead

  • Over-constraining generics — <T extends string | number> when just <T> would work; only constrain when you need specific operations

  • Not letting TypeScript infer type arguments — identity<string>('hello') is redundant; identity('hello') infers string automatically

  • Forgetting that generic type parameters are erased at runtime — you can't do if (T === string) or new T() at runtime

  • Creating 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