TypeScript Generics in React: The Patterns You'll Use Every Week

TypeScript generics in React feel intimidating right up until the moment they don't. The syntax is the hard part — the patterns themselves are straightforward once you see them three or four times. Here are the five you'll reach for every week.
Most TypeScript developers hit generics in React at the same moment: they're trying to type a component that works with any data, not just one specific shape, and any feels wrong. They look at the syntax — <T extends object> inside angle brackets that already have JSX angle brackets — and close the tab.
The syntax is genuinely awkward. JSX and TypeScript generics both use <>, and the TypeScript compiler can't always tell which is which. But the patterns themselves are simple once you separate the concept from the syntax noise.
Five patterns. These are the ones you'll actually use.
Pattern 1: Generic components
A List component that renders any array of items. Without generics, you'd write one for users, one for products, one for notifications. With generics, you write one that works for all of them.
The T is a type parameter — a placeholder for whatever type the caller passes in. When you use List, TypeScript infers T from what you pass to items:
user inside renderItem is typed as User automatically. If you try to access user.nonexistent, TypeScript catches it.
The JSX syntax problem. In a .tsx file, <T> looks like a JSX element. The TypeScript compiler gets confused. Two solutions:
The constraint approach is cleaner and more common — it also communicates that T must be an object, which is usually true for React list items anyway.
Pattern 2: Generic hooks
The hooks from Article #4 used generics already — useLocalStorage<T> and useDebounce<T>. Here's why and how.
A useLocalStorage hook without generics:
TypeScript infers T from initialValue:
The as const at the return ensures the tuple type is preserved — without it, TypeScript infers Array<T | Function> instead of [T, (value: T) => void].
Pattern 3: ComponentProps — extracting prop types
You have a third-party component or a component you've written. You want to pass all its props through another component without rewriting the entire type definition.
Every native button prop — onClick, type, aria-label, form, name — is automatically available on CustomButton without being explicitly declared. When the browser adds a new attribute or you need to pass something obscure, it just works.
ComponentPropsWithoutRef<'button'> is the version that strips the ref prop — useful when you're not forwarding refs. ComponentPropsWithRef<'button'> explicitly includes it.
Pattern 4: The as prop — polymorphic components
A Text component that renders as h1, h2, p, span, or any other HTML element — with the correct props for whichever element it becomes.
This is the pattern design systems use for components like <Box>, <Text>, and <Heading>.
Usage:
The type machinery here is more complex than the other patterns — but it's worth understanding because it's the foundation of every well-typed design system component. The key insight: C is the element type, and React.ComponentPropsWithoutRef<C> gives you the props for that specific element.
Pattern 5: Constraining generics
Sometimes T can be anything. Sometimes it needs to have specific properties. Constraints let you require structure without locking in a specific type.
Now SelectList works with any object that has id and label — but TypeScript knows what option is inside onChange, preserving all the extra properties:
Without the constraint, T could be anything — including a primitive that has no id. With T extends { id: string; label: string }, TypeScript enforces the minimum shape while still preserving the full type of whatever you pass in.
Putting it together: a typed data table
Here's a component that combines Pattern 1 (generic component), Pattern 3 (ComponentProps), and Pattern 5 (constrained generics) into something you'd actually ship:
Used like this:
TypeScript infers T = Article from the data prop. The key field in each column definition is typed as keyof Article — so passing key: 'nonexistent' is a compile-time error. The render function receives the correctly typed value for that column and the full row as Article.
One component. Works for any data shape with an id. Fully typed at every call site.
The syntax cheat sheet
Up next: Compound components — the design pattern that changed how I build UIs — the context-powered pattern behind every well-designed component API.
Related: TypeScript utility types you actually use — the built-in types that compose with generics to eliminate boilerplate.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Peaky Frames on Unsplash

