Iāll be honest, I avoided generics for way too long. Every time I saw <T> in someoneās code, Iād think āthatās for library authors, not for me.ā Turns out, I was wrong. Very wrong.
Last month, while working on Novahair, I had to refactor a Select component for the third time because each new use case needed slightly different types. Thatās when it clicked - I needed generics.
The Problem I Kept Running Into
Hereās what I used to do (and maybe you do too):
interface SelectProps {
options: Array<{ label: string; value: string }>;
onChange: (value: string) => void;
value: string;
}
function Select({ options, onChange, value }: SelectProps) {
// ... component logic
}
Looks fine, right? Until you need to use it with numbers. Or objects. Or literally anything that isnāt a string.
So Iād create SelectNumber, SelectObject, SelectWhatever. Copy-paste hell. Not great.
The solution
Generics let you write the component once and let TypeScript figure out the types based on usage. Hereās the same component, but actually reusable:
interface SelectProps<T> {
options: Array<{ label: string; value: T }>;
onChange: (value: T) => void;
value: T;
}
function Select<T>({ options, onChange, value }: SelectProps<T>) {
return (
<select
value={String(value)}
onChange={(e) => {
const selected = options.find(
opt => String(opt.value) === e.target.value
);
if (selected) onChange(selected.value);
}}
>
{options.map((opt) => (
<option key={String(opt.value)} value={String(opt.value)}>
{opt.label}
</option>
))}
</select>
);
}
Now TypeScript knows what type youāre working with:
// TypeScript knows value is a number
<Select<number>
options={[
{ label: "One", value: 1 },
{ label: "Two", value: 2 }
]}
onChange={(val) => console.log(val + 1)} // val is number!
value={1}
/>
// TypeScript knows value is a User object
<Select<User>
options={users.map(u => ({ label: u.name, value: u }))}
onChange={(user) => console.log(user.email)} // user is User!
value={currentUser}
/>
No more as casting. No more any. Just⦠works.
Real-World Example: A Table Component
This is where generics really shine. I built a Table component for Pol-UI that works with any data shape:
interface Column<T> {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface TableProps<T> {
data: T[];
columns: Column<T>[];
onRowClick?: (row: T) => void;
}
function Table<T>({ data, columns, onRowClick }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, idx) => (
<tr key={idx} onClick={() => onRowClick?.(row)}>
{columns.map((col) => (
<td key={String(col.key)}>
{col.render
? col.render(row[col.key], row)
: String(row[col.key])
}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Usage is beautiful:
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
<Table<User>
data={users}
columns={[
{ key: "name", header: "Name" },
{ key: "email", header: "Email" },
{
key: "isActive",
header: "Status",
render: (isActive) => isActive ? "ā
" : "ā"
}
]}
onRowClick={(user) => navigate(`/users/${user.id}`)}
/>
TypeScript autocompletes everything. The key must be a valid property of User. The render function gets the right type. The onRowClick receives a full User object.
Itās like magic, but itās just TypeScript doing its job.
Tips I Wish I Knew Earlier
1. You can constrain generics:
// Only allow objects with an 'id' property
function List<T extends { id: string | number }>(props: { items: T[] }) {
return props.items.map(item => <div key={item.id}>...</div>);
}
2. Default generic types are your friend:
// Defaults to string if not specified
function Input<T = string>({ value, onChange }: {
value: T;
onChange: (val: T) => void;
}) {
// ...
}
3. Multiple generics are totally fine:
function KeyValueList<K, V>({ items }: {
items: Array<{ key: K; value: V }>
}) {
// ...
}
When NOT to Use Generics
Real talk: donāt use generics just because you can. Iāve seen (and written) over-engineered components that wouldāve been simpler with a union type or just two separate components.
Use generics when:
- Youāre repeating the same component with different types
- Youāre building a library/design system
- The component truly doesnāt care about the data shape
Donāt use generics when:
- The component has specific business logic for specific types
- Youāre only using it in one place
- It makes the code harder to understand for your team
Wrapping Up
Generics went from āscary advanced TypeScriptā to āhow did I live without thisā in about a week of actually using them. Start small - maybe a List or Select component - and youāll quickly see where else they fit.
And hey, if you mess up, TypeScript will tell you. Thatās the whole point.
Now go make your components actually reusable. Your future self will thank you.
Related Articles
React Component Composition Patterns: Compound Components, Render Props & More š§©
Master React component composition patterns with real-world examples. Learn compound components, render props, slots, and context patterns. Reduce props, increase flexibility, and build better design systems.
React 19 New Features: Actions, use Hook, and Migration Guide š
Complete React 19 guide covering new features: Actions API, use hook, document metadata, ref as prop, and more. Includes migration tips, breaking changes, and real-world examples from production apps.
Why Use a UI Component Library in React? Benefits & Best Practices š
Discover why UI component libraries accelerate React development. Compare popular libraries, learn when to build vs buy, and explore best practices for design systems and component reusability.