Chapter 5.1☕ 16 min read

What are Generics?

Write once, use for many types, full type safety.

01The Biryani Handi

Generics are TypeScript's most powerful feature for writing REUSABLE, TYPE-SAFE code. Instead of writing the same logic for different types, you write it ONCE with a placeholder type, and fill in the actual type when you use it. This concept is the backbone of robust TypeScript libraries and applications.

Think of the Biryani Handi (pot). The handi itself doesn't care what goes inside — chicken biryani, mutton biryani, veg biryani, egg biryani. The HANDI is the generic container. The FILLING is the type parameter. When you say "give me a chicken biryani handi," you're specifying the type: Handi<Chicken>. When you say "give me a veg biryani handi," it's Handi<Veg>. The cooking METHOD is the same — layer the rice, add the filling, seal with dum, cook on slow fire. But the RESULT depends on what you put in.

That's generics — one recipe, many types, full type safety. Without generics, you'd need a separate chicken handi, mutton handi, veg handi... wasteful! One handi, any filling — that's the power! The compiler knows exactly what's inside your handi at all times. It won't let you accidentally put chicken where you expect veg, and it will always give you back the exact type you put in.

02The Problem Without Generics

Let's see the DRY violation problem step by step. Without generics, you write duplicate functions for every single type. This is tedious, error-prone, and completely violates the "Don't Repeat Yourself" principle that every developer should follow.

Duplicate Functions:

function firstNumber(
  arr: number[]
): number | undefined {
  return arr[0];
}

function firstString(
  arr: string[]
): string | undefined {
  return arr[0];
}

Same logic, different types! You could use any, but that KILLS type safety:

function firstAny(
  arr: any[]
): any {
  return arr[0];
}

const result = firstAny([1, 2, 3]);
// result is any, NOT number!
// No autocomplete, no type checking!

You could also use unions, but you lose precision:

function firstUnion(
  arr: (string | number)[]
): string | number | undefined {
  return arr[0];
}
// Return is ALWAYS string | number,
// even if you passed only strings!

Generics solve ALL these problems: ONE function, PRESERVED type information, NO duplication, FULL safety.

function first<T>(
  arr: T[]
): T | undefined {
  return arr[0];
}

T captures the actual type used at call time! If you pass number[], T is number. If you pass string[], T is string. The return type perfectly matches the input element type, every single time. No guessing, no casting, no any. This is the power of generics — precise, safe, and reusable.

03The Type Parameter <T>

The <T> is a TYPE PARAMETER — a placeholder for a type that will be specified later. It's like a variable, but for types, not values. You declare it in angle brackets, and then use it throughout the function signature and body. TypeScript ensures that every place you use T, it refers to the same concrete type that was passed in.

T is the conventional name (short for "Type"), but you can use any name: <Item>, <TValue>, <TResult>. Use descriptive names when the generic has a specific role, and single letters for simple cases.

Type Inference at Call Time:

first([1, 2, 3]);
// TS infers: T = number
// Return: number | undefined

first(["a", "b"]);
// TS infers: T = string
// Return: string | undefined

Explicit Specification:

first<string>(["a", "b"]);
// You tell TS: T is string

T flows through the ENTIRE function. If input is T[], and you return T, the output type matches the input element type PERFECTLY. No any, no unions, no guessing. The type information is preserved end-to-end.

Multiple Type Parameters:

function pair<T, U>(
  a: T,
  b: U
): [T, U] {
  return [a, b];
}

pair("chai", 99);
// T = string, U = number
// Return: [string, number]

When a function works with two independent types, you use <T, U>. Each type parameter is a separate placeholder that gets filled independently. Like ordering at an Irani cafe — your chai is one type, your Osmania biscuit is another type, but they both come together on the same table!

04Generic Traps

Generics are powerful, but they come with traps that catch beginners and even intermediate developers off guard. Let's explore the most common mistakes so you can avoid them in your code and spot them in code reviews.

Trap 1: Using any instead of generics.

function firstAny(
  arr: any[]
): any {
  return arr[0]; // Type info GONE!
}

function first<T>(
  arr: T[]
): T | undefined {
  return arr[0]; // Type info KEPT!
}

If you're tempted to use any, ask yourself: "Can I make this a type parameter?" Almost always, the answer is yes. Using any is like cooking biryani without any spices — sure, it's technically food, but you've lost everything that makes it special!

Trap 2: Treating T as a runtime value.

function make<T>(): T {
  // WRONG! T is a TYPE, not a value!
  // return new T();

  // T doesn't exist at runtime!
  return {} as T;
}

T is a TYPE parameter, not a value. You can't do new T() or instanceof T. T doesn't exist at runtime — it's completely erased during compilation. TypeScript uses it only for type checking, not for runtime logic.

Trap 3: Over-specifying — making things generic unnecessarily.

// WRONG: Only works with numbers!
function add<T>(a: T, b: T): T {
  // Can't add generic types!
  return a + b; // ERROR!
}

// RIGHT: Just use the specific type!
function add(
  a: number,
  b: number
): number {
  return a + b; // Perfect!
}

If a function only works with one type, don't make it generic! Not everything needs to be generic. A cup for Irani chai doesn't need to be a generic container — it's specifically for chai!

Trap 4: Forgetting the angle brackets.

// WRONG! Missing <T> declaration!
function first(T: T[]): T {
  return T[0];
}

// CORRECT! <T> declares the param!
function first<T>(arr: T[]): T {
  return arr[0];
}

The <T> before the parentheses DECLARES the type parameter. Without it, TypeScript thinks T is a variable name, not a type, leading to confusing errors.

Trap 5: Generic type parameter hiding. If an outer scope has T and your function also declares <T>, the inner one shadows the outer. Use descriptive names like <TItem> or <TResult> to avoid accidental shadowing in complex codebases.

05Generics Basics Cheatsheet

Here's your complete cheatsheet for generics basics. Pin this to your mental whiteboard — it captures everything you need to remember about type parameters, their syntax, and when to use them.

Concept: Type parameters — placeholders for types filled in at call time.

Syntax:

// <T> DECLARES the type parameter
// T USES the type parameter
function first<T>(arr: T[]): T {
  return arr[0];
}

Inference: TypeScript infers T from arguments.

first([1, 2, 3]); // T = number
first(["a"]);      // T = string

Explicit: You specify T manually.

first<string>(["a"]);

Multiple Type Parameters:

function pair<T, U>(
  a: T,
  b: U
): [T, U] {
  return [a, b];
}

Why Generics:

  • DRY — write once, use for many types
  • Type-safe — preserved type info, no any
  • Flexible — works with any type
  • Precise — return type matches input type exactly

When NOT to Use:

  • Function works with one type only (e.g., add(a: number, b: number))
  • You're overcomplicating simple code that doesn't need flexibility
  • The generic parameter adds no real value or type safety

The Golden Rule: Generics are the Biryani Handi — one pot, any filling, same dum method. Don't make a new handi for every biryani. But also don't use a handi when a cup will do, bhai!

Key Points

  • Generics let you write reusable, type-safe code — one recipe, many types
  • The is a type parameter — a placeholder filled in at call time
  • TypeScript infers the type from arguments, or you can specify it explicitly
  • Generics preserve type info perfectly, unlike any or union approaches
  • Don't overuse generics — if a function only works with one type, keep it simple
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase