Available for freelance work·Book a call
All articles/TypeScript Generics Without the Headache
TypeScriptFrontendBackend

TypeScript Generics Without the Headache

February 19, 20254 min read

TypeScript generics have a reputation for being confusing. The syntax is dense, the error messages are cryptic, and most tutorials jump straight to examples that require three nested generic parameters to even explain.

Here's the mental model that made them finally click for me.

Generics are just variables for types

That's it. A generic T is a placeholder — the same way x is a placeholder in a function. When you call the function with a specific value, x becomes that value. When you call a generic function with a specific type, T becomes that type.

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

identity(42);        // T = number
identity("hello");   // T = string

TypeScript infers T from the argument. You rarely need to specify it explicitly.

The pattern I use most: generic API wrappers

The most practical use of generics for me is wrapping API calls so the return type matches whatever the caller expects:

async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

type User = { id: number; name: string };

const user = await fetchJson<User>("/api/users/1");
// user is typed as User — no manual casting downstream

This pattern eliminates any from data-fetching code and pushes the type contract to the call site where you actually know what you're expecting.

Constraints: extends narrows what T can be

Sometimes you need to accept any type, but you need to guarantee it has certain properties:

function getFirstName<T extends { firstName: string }>(obj: T): string {
  return obj.firstName;
}

T extends { firstName: string } means "T can be anything, as long as it has at least a firstName string property." You get type safety on the property access without locking down the entire type.

I use this constantly for utility functions that operate on a subset of an object's fields.

keyof T: the pattern for safe property access

Paired with extends, keyof lets you write functions that accept a property name as an argument with full type safety:

function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Ahmad", role: "admin" };
const name = pluck(user, "name");  // typed as string
const id = pluck(user, "id");      // typed as number
// pluck(user, "email") — TypeScript error: not a valid key

K extends keyof T constrains key to only valid property names on T. The return type T[K] is the exact type of that property — not unknown, not any.

When not to use generics

The most common mistake I see is reaching for generics when a union type is clearer:

// Overengineered
function process<T extends string | number>(value: T): T { ... }

// Just use a union
function process(value: string | number): string | number { ... }

Generics add value when the return type needs to match the input type, or when you're building a reusable abstraction used in many places. For a one-off function, they're noise.

The rule I follow

If I can describe what a function does without mentioning types, it's probably a good candidate for generics. "This function takes any object and returns a specific property of it" — that's generic. "This function formats a date string" — that's not.


Generics clicked for me the moment I stopped thinking of them as a TypeScript feature and started thinking of them as type-level functions. They take type inputs and produce type outputs, just like regular functions take value inputs and produce value outputs. Once that clicked, the syntax stopped looking like line noise.