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 Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Utility Types: The Complete Guide
A comprehensive guide to all built-in TypeScript utility types including Partial, Required, Pick, Omit, Record, Extract, Exclude, and more with practical examples.
- TypeScript The Builder Pattern in TypeScript With Type Safety
Implement the builder pattern in TypeScript with compile-time enforcement of required fields using generics and conditional types.
- TypeScript Variance Annotations (in/out) for TypeScript Generics
Understand covariance, contravariance, and how TypeScript's in and out annotations make generic type relationships explicit.