Generic Components
A generic component is a component that works correctly with multiple data types while preserving full type safety. Instead of accepting any[] and losing all type information, a generic component uses a type parameter T that TypeScript resolves at the call site — giving you autocomplete, error detection, and precise return types for every usage.
The Problem Generics Solve
Imagine you are building a Select dropdown. You need it to work with strings, numbers, and custom objects. Without generics, you either write the same component three times or accept any, losing type safety:
// ✗ Too wide — value and onChange lose all type information
interface BadSelectProps {
options: any[]
value: any
onChange: (v: any) => void
}
// ✓ Generic — T is resolved by the caller
interface SelectProps<T> {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
}A Generic Select Component
Here is the complete typed Select component. TypeScript infers T from the options prop — no explicit type argument needed at the call site:
interface SelectProps<T> {
options: T[]
value: T | null
onChange: (value: T) => void
getLabel: (option: T) => string
getKey: (option: T) => string | number
placeholder?: string
}
function Select<T>({
options,
value,
onChange,
getLabel,
getKey,
placeholder = 'Select...',
}: SelectProps<T>) {
return (
<select
value={value ? getKey(value).toString() : ''}
onChange={(e) => {
const selected = options.find(o => getKey(o).toString() === e.currentTarget.value)
if (selected !== undefined) onChange(selected)
}}
>
<option value="" disabled>{placeholder}</option>
{options.map(option => (
<option key={getKey(option)} value={getKey(option).toString()}>
{getLabel(option)}
</option>
))}
</select>
)
}
// Usage with strings — T is inferred as string
const colors = ['Red', 'Green', 'Blue']
<Select
options={colors}
value={selectedColor}
onChange={setSelectedColor}
getLabel={c => c}
getKey={c => c}
/>
// Usage with objects — T is inferred as { id: number; name: string }
interface Country {
id: number
name: string
code: string
}
<Select<Country>
options={countries}
value={selectedCountry}
onChange={setSelectedCountry}
getLabel={c => c.name}
getKey={c => c.id}
/>A Generic List Component
A generic list component separates the rendering logic (your responsibility) from the iteration and key management (the component's responsibility):
import { ReactNode } from 'react'
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => ReactNode
keyExtractor: (item: T) => string | number
emptyMessage?: string
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'No items' }: ListProps<T>) {
if (items.length === 0) {
return <p className="empty">{emptyMessage}</p>
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
)
}
// With users — T = User, renderItem provides a User
interface User {
id: string
name: string
avatar: string
}
<List<User>
items={users}
keyExtractor={u => u.id}
renderItem={u => (
<div>
<img src={u.avatar} alt={u.name} />
<span>{u.name}</span>
</div>
)}
emptyMessage="No users found"
/>
// With plain strings — T is inferred as string
<List
items={['Apple', 'Banana', 'Cherry']}
keyExtractor={s => s}
renderItem={s => <strong>{s}</strong>}
/>Constrained Generics
Sometimes you need to guarantee that the type has certain properties. Use extends to constrain T:
// T must have at least an 'id' property of type string
interface TableProps<T extends { id: string }> {
data: T[]
columns: {
key: keyof T
header: string
render?: (value: T[keyof T], row: T) => ReactNode
}[]
}
function Table<T extends { id: string }>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id}> {/* row.id is safe because of the constraint */}
{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 — Product must have 'id: string' to satisfy the constraint
interface Product {
id: string
name: string
price: number
inStock: boolean
}
<Table<Product>
data={products}
columns={[
{ key: 'name', header: 'Name' },
{ key: 'price', header: 'Price', render: v => `$${v}` },
{ key: 'inStock', header: 'Stock', render: v => v ? '✓' : '✗' },
]}
/>Generic Custom Hooks
Generics are not limited to components — custom hooks benefit just as much:
import { useState, useCallback } from 'react'
// Generic hook for async data fetching
interface AsyncState<T> {
data: T | null
loading: boolean
error: string | null
}
function useAsync<T>(fetcher: () => Promise<T>): AsyncState<T> & { refetch: () => void } {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: false,
error: null,
})
const refetch = useCallback(async () => {
setState(s => ({ ...s, loading: true, error: null }))
try {
const data = await fetcher()
setState({ data, loading: false, error: null })
} catch (e) {
setState({ data: null, loading: false, error: String(e) })
}
}, [fetcher])
return { ...state, refetch }
}
// T is inferred from the fetcher's return type
const { data, loading, error } = useAsync<User>(() => fetchUser(userId))
// data is User | null — fully typedGeneric with Default Type
You can provide a default type for a generic parameter, making the type argument optional at the call site:
// T defaults to string — callers that don't specify T get string behavior
interface InputProps<T = string> {
value: T
onChange: (value: T) => void
parse?: (raw: string) => T
format?: (value: T) => string
}
function Input<T = string>({
value,
onChange,
parse = v => v as unknown as T,
format = v => String(v),
}: InputProps<T>) {
return (
<input
value={format(value)}
onChange={(e) => onChange(parse(e.currentTarget.value))}
/>
)
}
// No type argument — defaults to string
<Input value={name} onChange={setName} />
// Explicit type — parses input as number
<Input<number>
value={count}
onChange={setCount}
parse={Number}
format={n => n.toString()}
/>The satisfies Operator
TypeScript 4.9 introduced the satisfies operator, which validates that a value matches a type without widening it. This is useful for type-safe configuration objects in React:
type RouteConfig = {
path: string
component: React.ComponentType
exact?: boolean
}
// ✗ Without satisfies — routes is widened to RouteConfig[]
// You lose autocomplete on route-specific properties
const routes: RouteConfig[] = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// ✓ With satisfies — each entry is still typed as its exact object shape
// TypeScript can see the literal path values for autocomplete
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
] satisfies RouteConfig[]
// TypeScript knows routes[0].path is '/' (literal) not just stringUse
function Component<T>syntax for generics — arrow functions need a trailing comma in.tsx:<T,>Constrain with
T extends SomeInterfacewhen you need to access specific propertiesTypeScript infers
Tfrom props at the call site — explicit type arguments are optionalGeneric hooks follow the same pattern — the return type reflects
Tsatisfiesvalidates without widening — ideal for config objects and lookup tables