TypeScript Generics
Generics let you write code that works over many types while still tracking which type it's working with. A function that returns the first element of an array shouldn't return any. It should return a string for a string[] and a User for a User[]. Generics make that relationship explicit: function first<T>(xs: T[]): T.
They're the backbone of typed libraries: Array<T>, Promise<T>, React's useState<T>, TanStack Query's useQuery, and Zod's inferred schemas. Once you're comfortable reading and writing them, TypeScript goes from annotating code to designing APIs that are hard to misuse.
TL;DR
- A type parameter (
<T>) is a placeholder filled in at each call site, usually by inference from the arguments. - Constraints (
<T extends HasId>) restrict whatTcan be and let you use its properties. K extends keyof Tties one parameter to the keys of another, the basis of type-safe property access.- Defaults (
<T = unknown>) make type parameters optional. consttype parameters (<const T>) infer literal types instead of widening.- Use a generic only when a type parameter relates two or more positions (input to output, or argument to argument). Otherwise a plain type is clearer.
Quick Example
A typed groupBy and a type-safe property picker:
No explicit type arguments are needed. TypeScript infers T = User and K = "admin" | "member" from the call.
Core Concepts
Type Parameters and Inference
TypeScript infers type arguments from the values you pass. You write them explicitly only when there's nothing to infer from, for example useState<User | null>(null) or new Map<string, number>().
Constraints
Unconstrained T could be anything, so you can't access properties on it. extends narrows what's allowed:
The return type keeps the full type (Map<string, Product>), not just HasId. That's the advantage over declaring the parameter as HasId[].
keyof and Indexed Access
K extends keyof T constrains a key to the keys of an object type, and T[K] gives the type of the property at that key. Together they express "a key of this object and the value it points to":
Generic Interfaces, Types, and Classes
E = Error is a default: Result<User> means Result<User, Error>.
const Type Parameters
By default, TypeScript widens literals: ["a", "b"] is inferred as string[]. A const type parameter (TS 5.0+) keeps the literal types, which is ideal for config and route definitions:
Generics Plus Conditional and Mapped Types
Generics become far more expressive combined with conditional types (T extends string ? A : B), infer, and mapped types ({ [K in keyof T]: ... }). Those techniques are covered in TypeScript advanced features.
Designing Generic APIs
- Let inference work. Order parameters so the ones TypeScript can infer from come first. Callers should almost never need to write
<...>. - Relate positions. A type parameter that appears only once (
function log<T>(x: T): void) adds nothing overunknown. Use generics to connect input to output or one argument to another. - Constrain as little as possible, but no less. Require exactly the properties you use, and let the full type flow through.
- Prefer overloads or unions for small fixed sets. If a function handles exactly
string | number, a union parameter is simpler thanT extends string | number.
Best Practices
Name Parameters Meaningfully in Complex Signatures
T, K, and V are fine for short utilities. In signatures with several parameters, use TData, TError, TKey so readers (and error messages) stay clear.
Return the Narrowest Useful Type
function toArray<T>(x: T | T[]): T[] gives callers precise element types. Returning unknown[] pushes casts onto every caller.
Keep Casts Inside the Implementation
Sometimes the implementation needs an as (as with {} as Record<K, T[]> above) because TypeScript can't prove what you know. Contain it inside the function so the public signature stays sound, and test the function.
Test Types, Not Just Values
For library code, use type-level tests (expectTypeOf in Vitest, or // @ts-expect-error assertions) to lock in inference behavior.
Common Mistakes
Generics That Should Be unknown
Returning a Generic You Can't Actually Produce
A generic that exists only in the return type is a disguised type assertion. Validate untrusted data with a schema library such as Zod.
Over-Constraining
<T extends { id: string; name: string; email: string }> when the function only reads id forces callers to supply fields it never uses. Constrain to what you need.
FAQ
When should I use a generic instead of any?
Almost always. any switches off checking and loses the type. A generic preserves it: first<T>(xs: T[]): T returns a User for a User[], while first(xs: any[]): any returns something the compiler knows nothing about.
Why can't I access a property on T?
Because an unconstrained T could be any type, including number or null. Add a constraint (T extends { name: string }) so TypeScript knows the property exists.
What does T extends keyof U mean?
It restricts T to the union of U's property names. For interface User { id: number; name: string }, keyof User is "id" | "name", so T can only be one of those string literal types.
How do generics work with React components?
Components can be generic: function List<T>(props: { items: T[]; render: (item: T) => ReactNode }). TypeScript infers T from items, so render receives correctly typed items. In .tsx files, arrow functions need <T,> to avoid being parsed as JSX.
Related Topics
- TypeScript — The language overview
- TypeScript Basics — Foundations before generics
- TypeScript Advanced Features — Conditional, mapped, and template literal types
- TypeScript Type Narrowing — Refining unions safely
- Zod — Generics-powered schema inference
- Type Systems — Parametric polymorphism in general