ESC

Type to search the knowledge base.

Generics in React Components

Type props that depend on item shape — lists, selects, and tables — with generic components, inference pitfalls, and TSX syntax quirks.

advanced3 min read
  • 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:

  1. Wrapper function that returns a generic component.
  2. Assert a generic component type.
  3. 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.”

Further reading

Related guides