Generic Props
Using TypeScript generics to create type-safe, reusable component APIs that infer from props.
interface ListProps {
items: unknown[];
onSelect: (item: unknown) => void;
renderItem: (item: unknown) => React.ReactNode;
}interface ListProps<T> {
items: T[];
onSelect: (item: T) => void;
renderItem: (item: T) => React.ReactNode;
}Generics let TypeScript infer the item type from the items array and enforce it across all related props. When you pass items={users}, TypeScript knows onSelect receives a User and renderItem receives a User, with zero manual type annotations needed.
unknown throws that away: every consumer must cast.
interface SelectProps {
options: Array<{ value: string; label: string }>;
value: string;
onChange: (value: string) => void;
}
// Any string accepted, no constraint:
// <Select value="banana" options={roleOptions} />
// Compiles fine even though "banana" isn't a roleinterface SelectProps<V extends string = string> {
options: Array<{ value: V; label: string }>;
value: V;
onChange: (value: V) => void;
}
// Constrained to valid option values:
// type Role = 'admin' | 'user' | 'guest';
// <Select<Role> value="banana" ... />
// Type error: "banana" is not assignable to RoleA generic V parameter constrains value, onChange, and options to the same type. With <Select<Role>>, TypeScript ensures only valid roles are passed as value. Without generics, any string is accepted, so value="banana" compiles fine even when your options are roles.
The extends string constraint ensures values are still string-based.
interface DataTableProps<T> {
data: T[];
columns: Array<{
key: string;
header: string;
render?: (item: T) => React.ReactNode;
}>;
onRowClick?: (item: T) => void;
}interface DataTableProps<T extends { id: string | number }> {
data: T[];
columns: Array<{
key: keyof T & string;
header: string;
render?: (item: T) => React.ReactNode;
}>;
onRowClick?: (item: T) => void;
}Two improvements: T extends { id: string | number } ensures every row has a stable identity for React keys. Without this, the table can't reliably render rows. keyof T & string constrains key to actual properties of T, so key: "naem" (typo) is a compile-time error.
The unconstrained version accepts items with no id and column keys that don't exist on the data.
interface ComboboxProps {
items: Array<{ id: string; label: string }>;
value: string | null;
onChange: (id: string) => void;
renderItem: (item: {
id: string;
label: string;
}) => React.ReactNode;
}interface ComboboxProps<T extends { id: string }> {
items: T[];
value: T | null;
onChange: (item: T) => void;
renderItem: (item: T) => React.ReactNode;
}
// TypeScript infers T from items:
// <Combobox items={users}
// value={selectedUser}
// onChange={setSelectedUser}
// renderItem={(u) => u.name} />The generic version lets consumers work with their own data types. items={users} makes TypeScript infer T = User, so onChange receives a User (not a bare string ID) and renderItem gets full User access.
The non-generic version forces all data into {id, label}, so consumers must transform their data to fit the component.
interface FieldProps<V> {
name: string;
value: V;
onChange: (value: V) => void;
validate?: (value: V) => string | undefined;
}
// No connection between name and form shape:
// <Field name="emial" value={...} />
// Typo compiles fine!interface FieldProps<
TForm extends Record<string, unknown>,
K extends keyof TForm & string,
> {
name: K;
value: TForm[K];
onChange: (value: TForm[K]) => void;
validate?: (value: TForm[K]) => string | undefined;
}
// <Field<SignupForm, 'email'>
// name="email"
// value={form.email}
// onChange={...} />
// "emial" → type error!Two type parameters create a type-safe link between the form shape and the field. K extends keyof TForm means name must be an actual key of the form type. TForm[K] ensures value and onChange match that field's type, so a number field can't accidentally receive a string.
This is the pattern used by Formik, React Hook Form, and TanStack Form.