TypeScript Generics with React
A practical guide to using TypeScript generics in React components, hooks, and props for safer, more reusable building blocks.
What you'll learn
- ✓Generic components
- ✓Generic hooks
- ✓Constraints with extends
- ✓Default type params
- ✓Inference tips
Prerequisites
- •Comfortable with JS
What and Why
Generics let you write code that works with many types while preserving the exact type at each call site. In React, this is the difference between a List<T> that infers T from your data and a List that forces any. With generics, the renderItem callback knows whether it is rendering a User or a Product, and your IDE autocompletes properties accordingly.
The payoff is real: fewer casts, fewer bugs at boundaries, and components that scale to new data shapes without rewrites.
Mental Model
A generic is a placeholder for a type that gets filled in later, like a function parameter but at the type level. When you write function identity<T>(x: T): T, the compiler chooses T based on the argument. In React, the same idea applies to components and hooks.
<List<User> items={users} renderItem={u => u.name} />
|
v
T = User
|
+--> items: User[]
+--> renderItem: (item: User) => ReactNode Hands-on Example
A reusable list component:
type ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyOf: (item: T) => string;
};
export function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyOf(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
Usage infers T automatically:
<List
items={users}
keyOf={(u) => u.id}
renderItem={(u) => <span>{u.name}</span>}
/>
Generic hooks follow the same pattern. A typed local storage hook:
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Use constraints when T must have a shape:
function pickId<T extends { id: string }>(rows: T[]) {
return rows.map((r) => r.id);
}
Common Pitfalls
A handful of mistakes recur:
- Arrow components and JSX ambiguity.
const C = <T>(p) => ...looks like JSX to the parser. Use<T,>or<T extends unknown>in.tsxfiles. - Over-constraining. Adding
extends objecteverywhere blocks valid inputs. Constrain only what you actually use. - Casting around inference failure. If TypeScript cannot infer
T, pass it explicitly:useLocalStorage<User>('u', u). Do not reach foras any. - Discarded generics. A type parameter that appears once in the signature provides no benefit. Either use it twice (input and output), or drop it.
- Wide returns. Returning a tuple? Use
as constso consumers see literal positions, notArray<T | Setter>.
Best Practices
Start concrete, then generalize. Write the component for one type first; once a second use case appears, extract a type parameter. Name parameters meaningfully (TItem, TData) when you have more than one. Provide defaults for ergonomic call sites: function useFetch<T = unknown>(url: string) lets users skip the type when they do not care.
Prefer inference over explicit type arguments. If you find yourself writing <User> everywhere, your callback signatures may be hiding the type from the compiler. Make sure prop types reference T directly so inference flows.
Document non-obvious constraints with comments. A type parameter that exists to enforce a relationship between two props (like keyof T in a column prop) deserves a one-line note.
Wrap-up
Generics turn React components into reusable, type-safe primitives. Master a few patterns (generic list, generic hook, constrained parameter), watch out for the JSX arrow gotcha, and you can build libraries that feel like they were written for each consumer.
Related articles
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.
- TypeScript TypeScript infer Keyword Explained
Master the infer keyword in TypeScript conditional types. Learn how to extract parts of complex types, build utility helpers, and write expressive generics.
- TypeScript TypeScript Mapped Types Deep Dive
A practical tour of mapped types in TypeScript, including key remapping, modifiers, and patterns that build powerful generic utilities.
- TypeScript TypeScript Recursive Types Tutorial
Build recursive types in TypeScript: deep readonly, JSON, paths, and tuple manipulation. Learn how to write recursion that the compiler can actually evaluate.