📖 The Story: The Copy-Paste Catastrophe

A fintech startup needed a function to find the first item in an array matching a condition. Simple enough, right?

First, they needed it for numbers:

typescript · The Problem
function findFirstNumber(arr: number[], predicate: (item: number) => boolean): number | undefined {
  for (const item of arr) {
    if (predicate(item)) return item;
  }
  return undefined;
}

Then they needed it for strings. Then for Users. Then for Transactions. Each time, they copied the function and changed the types. The codebase had 11 nearly identical functions. When a bug was found in the logic, they had to fix it in 11 places. They missed 3. Production broke.

The CTO rewrote all 11 functions as one generic function in 5 lines. The bug? Fixed once. Forever.

Duplicating code for different types is a design flaw. Generics are the cure.

🎯 Why Generics?

♻️

Write Once

One implementation that works with strings, numbers, objects—anything.

🛡️

Type Safe

Unlike any, generics preserve the exact type. Full autocomplete works.

🐛

Fix Once

Bugs are fixed in one place, not in 11 copy-pasted variants.

🧒 Explain Like I'm 10

Imagine a cookie cutter. It's just a shape—a star, a heart, a circle. You don't buy a "star cookie cutter for chocolate" and a "star cookie cutter for vanilla." You buy one star cutter and use it with any dough.

A generic function is that cookie cutter. The "dough" is the type you pass in. The cutter (function) doesn't care what dough it gets—it produces perfectly shaped cookies (typed results) every time.

🎓 Professional Explanation

Generics are type variables. When you write function identity<T>(value: T): T, you're declaring a type parameter T that acts as a placeholder. When the function is called, TypeScript infers (or you explicitly specify) what T is, and every occurrence of T is replaced with that type. This preserves type information throughout the call chain.

📊 How Generics Work

One Function, Many Types

📝
Call with string

identity<string>("hello")

T = string

🔢
Call with number

identity<number>(42)

T = number

📦
Call with User

identity<User>(user)

T = User

Same function. Different types. Full safety in every case.

🔴 The Problem (Before Generics)

There are two bad ways to handle "works with any type":

typescript · Both Bad
// Option 1: any — LOSES all type safety
function firstItem(arr: any[]): any {
  return arr[0];
}

const item = firstItem(["a", "b"]);
item.toFixed(2); // No error! Crashes at runtime (strings don't have toFixed)

// Option 2: Function overloads — EXPLODES with each new type
function firstItemStr(arr: string[]): string { return arr[0]; }
function firstItemNum(arr: number[]): number { return arr[0]; }
// ... repeat for every type in existence

🔧 The Generic Solution

typescript · The Fix
// T is a type parameter — a placeholder for "whatever type you give me"
function firstItem<T>(arr: T[]): T | undefined {
  return arr[0];
}

// TypeScript INFERS that T = string here
const str = firstItem(["a", "b", "c"]);
str.toUpperCase(); // ✅ TS knows str is a string!
str.toFixed(2);  // ❌ Error! toFixed doesn't exist on string

// TypeScript INFERS that T = number here
const num = firstItem([1, 2, 3]);
num.toFixed(2); // ✅ TS knows num is a number!

// You can also specify T explicitly (usually unnecessary)
const explicit = firstItem<string>(["x"]);

The magic: T flows through the entire function. Whatever goes in as T[] comes out as T. No any, no overloads, no duplication. Full IntelliSense.

📦 Generic Interfaces

Interfaces can be generic too. This is the foundation of the Array<T> type you use every day.

typescript
// A generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// Use it with any data shape
interface User { id: number; name: string; }

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Ram" },
  status: 200,
  message: "Success"
};

userResponse.data.name; // ✅ Autocomplete knows this is a string

🔒 Constraints with extends

Sometimes you need T to have certain capabilities. Use extends to constrain it.

typescript
// Without constraint: Error! Not every type has .length
function getLength<T>(value: T): number {
  return value.length; // ❌ Property 'length' does not exist on type 'T'
}

// With constraint: T must have a length property
function getLength<T extends { length: number }>(value: T): number {
  return value.length; // ✅ Now TS knows .length exists!
}

getLength("hello"); // ✅ 5 (strings have length)
getLength([1, 2, 3]); // ✅ 3 (arrays have length)
getLength(42); // ❌ Error! Numbers don't have length

🤝 Multiple Type Parameters

typescript
// Two type parameters: K for key type, V for value type
function merge<K extends string, V>(key: K, value: V): Record<K, V> {
  return { [key]: value } as Record<K, V>;
}

const result = merge("id", 123);
// result: { id: number }

⚠️ Common Mistakes

Mistake 1: Using any Inside Generics

typescript · Bad
// Bad: Declares T but never uses it — just uses any
function bad<T>(value: any): any {
  return value;
}

// Good: Actually uses T
function good<T>(value: T): T {
  return value;
}

✅ Best Practices

  1. Name descriptively: Use T for single params, but TKey/TValue for clarity in complex APIs.
  2. Let TypeScript infer: Don't explicitly pass <string> unless inference fails.
  3. Constrain when possible: T extends { id: string } is safer than unconstrained T.
  4. Don't over-genericize: If a function only ever works with User, don't make it generic.

🏗️ Senior Deep Dive

Array<T> Is Just a Generic Interface

The humble string[] you write every day is actually syntactic sugar for Array<string>, which is a generic interface:

typescript · lib.es5.d.ts (Simplified)
interface Array<T> {
  length: number;
  push(...items: T[]): number;
  pop(): T | undefined;
  map<U>(callbackfn: (value: T, index: number) => U): U[];
  // Notice: map takes a DIFFERENT generic (U) for its return!
}

Chain of generics: map on Array<T> introduces a new generic U. This is how [1,2,3].map(x => x.toString()) correctly returns string[]—TypeScript infers U = string from your callback.

💼 Interview Questions

Beginner
What problem do generics solve?
Show Answer
Generics eliminate code duplication while preserving type safety. Without them, you either use any (losing safety) or write separate functions for each type (losing maintainability).
Intermediate
What does T extends { length: number } mean?
Show Answer
It's a generic constraint. It restricts T to only types that have a length property of type number (strings, arrays, etc.). This lets you safely access value.length inside the function.
Advanced
Explain how Array.prototype.map uses two generics.
Show Answer
map<U>(callback: (value: T) => U): U[]T comes from the array itself (what's in it), while U is inferred from the callback's return type. This is why [1,2,3].map(String) returns string[].

🏋️ Practical Exercise

🎯 Hands-On Practice Medium

Build a Generic Function

Write a generic function pluck that takes an array of objects and a property name, returning an array of that property's values.

typescript · Goal
// Should work like this:
const users = [
  { name: "Ram", age: 30 },
  { name: "Alex", age: 25 }
];

pluck(users, "name"); // ["Ram", "Alex"] — type: string[]
pluck(users, "age");  // [30, 25] — type: number[]
pluck(users, "email"); // ❌ Error! email doesn't exist on User

Hint: Use two type parameters: <T, K extends keyof T>.

📝 Quiz

Test your understanding. Click an option to check your answer.

1. What does <T> represent in function foo<T>(x: T): T?
A) A specific type called T
B) A type parameter (placeholder) filled in when the function is called
C) A tuple type
Answer: B — T is a placeholder that TypeScript replaces with the actual type based on usage.
2. What's the main advantage of generics over using any?
A) Generics are faster at runtime
B) Generics preserve type information, giving you autocomplete and safety
C) Generics are required by the compiler
Answer: B — any throws away the type. Generics track it, so firstItem(["a"]) returns string, not any.
3. What keyword constrains a generic type?
A) implements
B) extends
C) constraint
Answer: B — T extends { length: number } requires T to have a length property.
4. What is string[] actually shorthand for?
A) List<string>
B) Array<string>
C) Collection<string>
Answer: B — Arrays are generic interfaces. string[] = Array<string>.
5. In arr.map<U>(cb: (v: T) => U): U[], what determines U?
A) The array's type
B) The return type of the callback function you pass
C) A global variable
Answer: B — TypeScript infers U from what your callback returns, enabling map to change types.

📦 Summary

🎯 Key Takeaways

  • Generics are type parameters (<T>) — placeholders filled in at call time.
  • They eliminate copy-paste duplication while keeping full type safety.
  • Use extends to constrain T to types with specific properties.
  • Generic interfaces (ApiResponse<T>) model reusable data wrappers.
  • Array<T> is a generic — you use generics every day!
  • Let TypeScript infer types; only specify <Type> explicitly when inference fails.