Skip to content
TypeScript ts react 4 min read

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.

SituationHow T is resolved
T appears in a value prop (items)inferred automatically
T only appears in children/callbackspass explicitly: <Comp<User> …>
T must have certain membersconstrain 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 T from a value prop; pass it explicitly only when it cannot.
  • Constrain T with extends when you must access specific members like id.
  • In .tsx, write generic arrows as <T,> to avoid the JSX parsing ambiguity.
  • Share one type parameter across related props to keep value, options, and onChange in sync.
  • Type render-prop children as (arg: T) => ReactNode so the caller’s callback is precise.
  • Keep generics minimal — add a parameter only when a real type relationship needs it.
Last updated June 29, 2026
Was this helpful?