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
The Vercel AI SDK: Streaming Chat and Tool Calls in TypeScript
The AI SDK is a TypeScript toolkit for calling language models from any provider with one API, streaming responses to React with useChat, calling tools, and generating structured data. A working Next.js chat with Claude, tool calling with stopWhen, structured output, provider choice, and production concerns.
useMemo vs useCallback (and React.memo): When Memoization Helps
useMemo caches a computed value; useCallback caches a function; React.memo skips re-rendering a component whose props didn't change. When each actually helps, when it's wasted effort, the dependency array rules, and how the React Compiler now does most of this for you.