Generics in React Components
Type props that depend on item shape — lists, selects, and tables — with generic components, inference pitfalls, and TSX syntax quirks.
- typescript
- generics-in
A <Select> that only works with string options is a toy. Real design-system and app components need props that scale with the caller’s data type: items of T, values of T['id'], render props that see T. That is generics on React components.
Docs: Generics, React types via @types/react.
Function component with type parameter
type ListProps<T> = {
items: T[];
getKey: (item: T) => string;
children: (item: T) => React.ReactNode;
};
function List<T>({ items, getKey, children }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{children(item)}</li>
))}
</ul>
);
}
// T inferred as User
<List
items={users}
getKey={(u) => u.id}
children={(u) => u.name}
/>;
Prefer function declarations for generic components — they parse cleanly in TSX.
Arrow components: trailing comma trick
const List = <T,>({ items, getKey, children }: ListProps<T>) => {
// trailing comma after T — otherwise TSX thinks <T> is a tag
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{children(item)}</li>
))}
</ul>
);
};
Alternatives: <T extends unknown> or a function declaration. Pick one team convention.
Controlled select with generic value
type SelectProps<T> = {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
};
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={value ? getValue(value) : ''}
onChange={(e) => {
const next = options.find((o) => getValue(o) === e.target.value);
if (next) onChange(next);
}}
>
<option value="" disabled>
Choose…
</option>
{options.map((o) => (
<option key={getValue(o)} value={getValue(o)}>
{getLabel(o)}
</option>
))}
</select>
);
}
Callers pass objects; the component never assumes a fixed shape beyond what getLabel / getValue need.
Constrained item shapes
type Entity = { id: string };
type TableProps<T extends Entity> = {
rows: T[];
columns: { key: keyof T & string; header: string }[];
};
function Table<T extends Entity>({ rows, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((c) => (
<th key={c.key} scope="col">
{c.header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
{columns.map((c) => (
<td key={c.key}>{String(row[c.key])}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
See generic constraints.
ForwardRef + generics (hard mode)
forwardRef and generics don’t compose elegantly. Patterns:
- Wrapper function that returns a generic component.
- Assert a generic component type.
- Avoid ref on the generic outer; forward on a non-generic inner.
type FancyProps<T> = { value: T; onChange: (v: T) => void };
function FancyInner<T>(
props: FancyProps<T> & { ref?: React.Ref<HTMLInputElement> },
) {
return (
<input
ref={props.ref}
value={String(props.value)}
onChange={() => {
/* map as needed */
}}
/>
);
}
For design systems, check current React type versions — patterns evolve with React.forwardRef typing improvements.
Inference failure modes
// Empty array — T becomes never or unknown depending on context
<List items={[]} getKey={() => ''} children={() => null} />
// Fix: annotate
<List<User> items={[]} getKey={(u) => u.id} children={(u) => u.name} />
Or provide a sample typed empty array: useState<User[]>([]).
Don’t generic-wash fixed props
If the component only ever handles string filters, type string. Generics are for preserved relationships between props (items ↔ render prop ↔ value).
Interview out-loud answer
“Generic components parameterize item/value types so lists and selects stay typed to the caller’s data. I use function declarations or <T,> arrows, constrain with extends when I need id, and explicitly pass type args when inference fails on empty data. forwardRef + generics needs a careful pattern.”
Related on this site
- Generics basics
- Generic constraints
- Typing React props
- Typing React events
- keyof typeof and indexed access
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.