Blog
4 min read

TypeScript Generics Explained (Without the Headache)

Generics let a function, type or component work with many types while keeping type safety — like 'a list of T'. How <T> works, inference, constraints with extends, keyof, default type parameters, generic React components and hooks, utility types like Partial and Pick, and when generics are overkill.

Generics let you write code that works with many types without losing track of which type it's working with. You've used them already: Array<string>, Promise<User>, useState<number>. (What is TypeScript?)

The problem they solve

A function that returns the first item of an array:

function first(items: any[]): any {
  return items[0]
}

const n = first([1, 2, 3])   // n is any — type information lost
n.toUpperCase()               // no error, crashes at runtime

With a generic:

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

const n = first([1, 2, 3])       // n: number | undefined
const s = first(['a', 'b'])      // s: string | undefined

T is a type parameter — a placeholder filled in when the function is used. "Give me an array of T, I'll give you a T back."

Inference: you rarely write the type yourself

TypeScript works T out from the arguments. You can be explicit (first<number>([1, 2])), but usually don't need to.

The main time you must specify it is when there's nothing to infer from:

const [users, setUsers] = useState<User[]>([])   // [] alone would be never[]

Constraints: extends

Sometimes T must have certain properties:

function byId<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

byId(users, 'u1')       // ✅ users have id
byId([1, 2, 3], 'x')    // ❌ number has no id

T extends { id: string } means "T can be any type, as long as it has a string id". The return type is still the full User, not just { id }.

keyof: type-safe property names

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key])
}

pluck(users, 'email')    // string[]
pluck(users, 'emial')    // ❌ typo caught

keyof T is the union of T's property names; T[K] is the type of that property.

Generic types and interfaces

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string }

async function getJson<T>(url: string): Promise<ApiResult<T>> { /* ... */ }

const result = await getJson<Invoice[]>('/api/invoices')
if (result.ok) result.data   // Invoice[]

Default type parameters work like default arguments: type Paginated<T, Cursor = string> = { items: T[]; next: Cursor | null }.

(A caution: getJson<Invoice[]> only tells TypeScript what the data is — it doesn't check. Validate external data at runtime, e.g. with Zod. (Validating input with Zod))

Built-in utility types

These are generics you'll use constantly:

Utility Gives
Partial<User> All properties optional — good for update payloads
Required<T> All properties required
Pick<User, 'id' | 'email'> Only some properties
Omit<User, 'passwordHash'> Everything except some — good for public API shapes
Record<string, number> An object map
ReturnType<typeof fn> What a function returns
Awaited<T> The resolved type of a Promise

Generic React components

type ListProps<T> = {
  items: T[]
  render: (item: T) => React.ReactNode
  getKey: (item: T) => string
}

export function List<T>({ items, render, getKey }: ListProps<T>) {
  return <ul>{items.map(i => <li key={getKey(i)}>{render(i)}</li>)}</ul>
}

<List items={users} getKey={u => u.id} render={u => u.email} />   // u is User

Generic hooks work the same way. (React custom hooks)

When generics are overkill

  • If a function only ever handles one type, use that type.
  • If you find yourself writing <T, U, V, W> with complex constraints, simplify — readable types beat clever ones.
  • Don't use a generic where a union type says it better ('small' | 'large'). (type vs interface)

Reading generic errors

Generic-heavy error messages are long. Read from the bottom up, and hover in your editor to see what T was inferred as. (Type is not assignable to type)


EasySpawn runs your TypeScript project on a server with Claude Code built in — it can explain a gnarly generic error and type a helper properly instead of reaching for any. See how it works or join the waitlist.

Related: type vs interface in TypeScript · Type Is Not Assignable to Type · JavaScript vs TypeScript · Why TypeScript Makes AI-Generated Code Safer

Keep reading