← Writing
How-to

TypeScript generics without the headache: a practical field guide

2026-02-10 · 4 min read

For about a year into using TypeScript seriously, I had an unspoken rule: if the solution involved generics, find a different solution.

It wasn't that I couldn't get the syntax to work. It was that I could never tell why it worked. I'd copy a pattern from Stack Overflow, it would compile, and I'd move on with zero understanding of what I'd just written. That's a bad place to be with a tool you're using every day.

Then someone explained generics to me with an analogy that immediately made them obvious. I want to pass that analogy on.

The analogy that made it click

Think of a generic as a placeholder for a type you don't know yet.

A function that takes a string and returns a string is specific — it only works with strings. But what if you want a function that works with any type, while still being type-safe? You need a placeholder.

// Without generics — only works with strings
function first(arr: string[]): string {
  return arr[0]
}
 
// With generics — works with any array
function first<T>(arr: T[]): T {
  return arr[0]
}

The T is the placeholder. When you call first([1, 2, 3]), TypeScript fills in T = number automatically. When you call first(['a', 'b', 'c']), it fills in T = string. You wrote one function that handles both, and TypeScript still knows the return type.

That's it. That's the core idea. Everything else is variations on this.

The three places I actually use generics

1. Functions that work with multiple types

The first example above is the canonical case. Any time you find yourself writing the same function twice for different types, generics are probably the answer.

// Before — duplicated logic
function getFirstString(arr: string[]): string { return arr[0] }
function getFirstNumber(arr: number[]): number { return arr[0] }
 
// After — one function
function getFirst<T>(arr: T[]): T { return arr[0] }

2. API response wrappers

This is where I use generics most in real work. We have a standard API response shape — status, data, error — and the data field is different for every endpoint.

interface ApiResponse<T> {
  status: number
  data: T
  error: string | null
}
 
// Usage
type UserResponse = ApiResponse<User>
type PostsResponse = ApiResponse<Post[]>

Without generics, you'd either use any (which defeats the point of TypeScript) or write a separate interface for every endpoint (which doesn't scale). Generics solve this cleanly.

3. Custom hooks that wrap data-fetching

If you're building a useFetch hook or anything similar, the return type depends on what you're fetching:

function useFetch<T>(url: string): { data: T | null; loading: boolean; error: string | null } {
  // implementation
}
 
// TypeScript now knows data is User | null
const { data } = useFetch<User>('/api/user/123')

Constraints — when T is too broad

Sometimes you don't want T to be anything. You want it to be anything that has certain properties. That's where extends comes in:

// T must have an id property
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

This works with users, posts, comments — anything with an id. But TypeScript will catch it at compile time if you pass in something that doesn't have id.

The mistake I kept making

I would over-constrain generics early — adding extends before I needed to, trying to be "safe". This made the code harder to read and the generics less useful.

The better approach: start with an unconstrained T, then add constraints only when the compiler tells you to. Let the errors guide the constraints. You'll usually need fewer than you think.

When not to use generics

Generics add cognitive overhead. For a function that will always take a string, just type it as string. Don't reach for generics because they feel more sophisticated — use them when you're actually working with multiple types.

The question I ask: would this code need to be duplicated if I didn't use generics? If yes, generics are probably right. If no, they're probably overkill.