When you build real-world apps, flexibility matters. You don’t want to rewrite the same logic for different types. You want to write once, use everywhere — safely.

That’s what generics in TypeScript are for.

They let you build type-safe, reusable, and intelligent functions, components, and data structures — without giving up precision.

Let’s break them down with real-world patterns you’ll actually use.


🔄 What are generics?

Generics are type placeholders — they let you write functions or components where the type is decided later.

Think of it as saying:

> “I don’t know the exact type yet, but whatever it is, I’ll work with it safely.”

Basic example:

function identity<T>(value: T): T {
  return value
}

identity(42)        // type: number
identity('hello')   // type: string

Here, T is a generic type variable. When you call the function, TypeScript infers the type for you.


💡 Why not just use any?

Because any removes all type safety.

function identity(value: any): any {
  return value.toUpperCase() // no error — even if it's a number
}

Generics preserve type information, so you get:

  • Autocomplete
  • Static checks
  • Safer refactoring

🧰 Real-world use cases

1. Reusable utility functions

function getFirst<T>(list: T[]): T | undefined {
  return list[0]
}

getFirst([1, 2, 3])     // type: number
getFirst(['a', 'b'])    // type: string

Works with any type — without losing context.


2. Generic props in react

type DropdownProps<T> = {
  options: T[]
  onSelect: (value: T) => void
}

function Dropdown<T>({ options, onSelect }: DropdownProps<T>) {
  return (
    <select onChange={e => onSelect(options[e.target.selectedIndex])}>
      {options.map((opt, i) => (
        <option key={i}>{String(opt)}</option>
      ))}
    </select>
  )
}

🟢 Now you can reuse this component for string[], number[], or even User[] — and still get full type safety.


3. API Response wrappers

type ApiResponse<T> = {
  data: T
  success: boolean
  error?: string
}

// Usage:
const response: ApiResponse<User[]> = {
  data: [{ id: 1, name: 'Jane' }],
  success: true,
}

🟢 Works for any backend shape, while maintaining strict types.


⛓️ Constraints (extends)

You can limit what a generic type can be:

function getLength<T extends { length: number }>(item: T) {
  return item.length
}

getLength('hello')         // ✅
getLength([1, 2, 3])        // ✅
getLength(123)             // ❌ number doesn't have .length

This helps you express:

> “I'll accept any type, as long as it has this shape.”

🧠 Tips for clean generic code

  • Use short names like T, U, K, V when the context is obvious
  • Be explicit when inference fails: identity<string>('hello')
  • Avoid overcomplicating — don’t add generics if a union or overload works better
  • Combine with utility types like Partial<T>, Pick<T>, Record<K, T>

❓ When not to use generics

Generics add complexity. You don’t need them when:

  • The function/component is truly type-specific
  • A union type would work better (string | number)
  • You’re not returning or using the input type meaningfully

🧩 The bottom line

Generics let your code adapt without becoming loose or fragile. They’re TypeScript’s answer to reusability with guardrails.

Use them for:

  • Building reusable logic
  • Wrapping backend responses
  • Designing flexible components
  • Working with collections

And always remember: you’re not just writing code — you’re shaping the developer experience and protecting the user experience. Generics help you do both, at scale.