Learn

/

Generic Props

Generic Props

5 patterns

Using TypeScript generics to create type-safe, reusable component APIs that infer from props.

Avoid
interface ListProps {
  items: unknown[];
  onSelect: (item: unknown) => void;
  renderItem: (item: unknown) => React.ReactNode;
}

Prefer
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.

TypeScript: Generics
Avoid
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 role

Prefer
interface 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 Role

A 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.

TypeScript: Generic Constraints
Avoid
interface DataTableProps<T> {
  data: T[];
  columns: Array<{
    key: string;
    header: string;
    render?: (item: T) => React.ReactNode;
  }>;
  onRowClick?: (item: T) => void;
}

Prefer
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.

TypeScript: Generic Constraints
Avoid
interface ComboboxProps {
  items: Array<{ id: string; label: string }>;
  value: string | null;
  onChange: (id: string) => void;
  renderItem: (item: {
    id: string;
    label: string;
  }) => React.ReactNode;
}

Prefer
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.

TypeScript: Type Parameters in Constraints
Avoid
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!

Prefer
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.

TypeScript: Indexed Access Types