Generic Components
A generic component is one whose props are parameterized by a type the caller supplies, letting a single List, Select, or Table work with any item shape while staying fully type-safe. The payoff is huge: the item in a render callback is typed as your actual data, the value returned by an onChange matches the options, and a wrong-shaped prop is a compile error. The mechanics are the same generic functions you already know, applied to components — with a couple of JSX-specific syntax wrinkles. This page covers generic function components, constraints, inference from props, and render-prop patterns.
A generic function component
Declare the type parameter on the component function, then use it in the props type. TypeScript infers the type argument from the props you pass, so callers rarely write it explicitly.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
// T is inferred as { id: number; name: string } from `items`:
<List
items={[{ id: 1, name: "Ada" }]}
renderItem={(user) => user.name} // user is fully typed
/>;
The type parameter T flows from the items array into renderItem, so user inside the callback has the exact item shape with autocomplete. Returning user.age here would be a compile error because the property does not exist.
Constraining the type parameter
Use extends to require that the type argument has certain members. A common need is a stable key, so constrain T to include an id.
interface HasId {
id: string | number;
}
function KeyedList<T extends HasId>({ items, renderItem }: ListProps<T> & { items: T[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{renderItem(item)}</li> // item.id is guaranteed
))}
</ul>
);
}
// ❌ Error: '{ name: string }' is missing 'id'
// <KeyedList items={[{ name: "x" }]} renderItem={(i) => i.name} />
The T extends HasId constraint lets you safely read item.id for the key, and rejects any item type that lacks an id. Without the constraint, item.id would not type-check.
The arrow-function syntax wrinkle
In .tsx files, a bare <T> arrow function is ambiguous with a JSX tag and fails to parse. Use a trailing comma <T,> (or extends unknown) to disambiguate. Function declarations have no such issue.
// ❌ In .tsx, <T> is parsed as a JSX element:
// const List = <T>(props: ListProps<T>) => { ... };
// ✅ Trailing comma resolves the ambiguity:
const List = <T,>(props: ListProps<T>) => {
return <ul>{/* ... */}</ul>;
};
This is purely a parser concern in .tsx; the <T,> and <T extends unknown> forms are identical to a plain <T> everywhere else.
Linking two props with one parameter
Generics shine when several props must agree. A typed Select ties the options, the selected value, and the onChange callback to one shared T.
interface SelectProps<T> {
options: readonly T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={getLabel(value)}
onChange={(e) => {
const next = options.find((o) => getLabel(o) === e.target.value);
if (next) onChange(next);
}}
>
{options.map((o) => <option key={getLabel(o)}>{getLabel(o)}</option>)}
</select>
);
}
Because value, onChange, and getLabel all reference the same T, passing a value that is not one of the options’ type — or an onChange expecting a different shape — is rejected. One type parameter enforces consistency across the whole API.
Render props and children-as-function
A generic render prop hands typed data back to the caller. Typing children as a function (item: T) => ReactNode makes the consumer’s callback parameter precise.
interface FetchProps<T> {
url: string;
children: (data: T | null) => React.ReactNode;
}
function Fetcher<T>({ url, children }: FetchProps<T>) {
const [data, setData] = useState<T | null>(null);
// ...fetch into setData
return <>{children(data)}</>;
}
// data is User | null inside the callback:
<Fetcher<User> url="/api/me">{(data) => <p>{data?.name}</p>}</Fetcher>;
Here inference cannot find T from the props alone, so the caller supplies it explicitly with <Fetcher<User>>. The data parameter is then typed User | null, giving the consumer safe, autocompleted access.
| Situation | How T is resolved |
|---|---|
T appears in a value prop (items) | inferred automatically |
T only appears in children/callbacks | pass explicitly: <Comp<User> …> |
T must have certain members | constrain with T extends Shape |
Best Practices
- Put the type parameter on the component (
function List<T>) and let it flow through the props. - Let inference resolve
Tfrom a value prop; pass it explicitly only when it cannot. - Constrain
Twithextendswhen you must access specific members likeid. - In
.tsx, write generic arrows as<T,>to avoid the JSX parsing ambiguity. - Share one type parameter across related props to keep
value,options, andonChangein sync. - Type render-prop
childrenas(arg: T) => ReactNodeso the caller’s callback is precise. - Keep generics minimal — add a parameter only when a real type relationship needs it.