Generic Components

A Reusable Typed List or Table

Components can be generic too. A DataTable<T> infers T from the rows prop, so column keys and render functions are checked against the row type.

import type { ReactNode } from "react";

type Column<T> = {
  key: keyof T;
  header: string;
  render?: (row: T) => ReactNode;
};

type DataTableProps<T> = {
  rows: T[];
  columns: Column<T>[];
  getRowId: (row: T) => string | number;
};

export function DataTable<T>({ rows, columns, getRowId }: DataTableProps<T>) {
  return (
    <table>
      <thead><tr>{columns.map((c) => <th key={String(c.key)}>{c.header}</th>)}</tr></thead>
      <tbody>
        {rows.map((row) => (
          <tr key={getRowId(row)}>
            {columns.map((c) => <td key={String(c.key)}>{c.render ? c.render(row) : String(row[c.key])}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// usage: T is inferred as Product
<DataTable
  rows={products}
  getRowId={(p) => p.id}
  columns={[
    { key: "name", header: "Product" },
    { key: "price", header: "Price", render: (p) => "Rs. " + p.price },
    // { key: "rating", header: "Rating" }  -> error: "rating" is not a key of Product
  ]}
/>;

Generic Arrow Components in .tsx

Note: In a .tsx file, const List = <T,>(props: ListProps<T>) => ... needs the trailing comma (or <T extends unknown>) so the parser does not read <T> as a JSX tag. A function declaration avoids the issue.