Type safety
The grid is typed from one thing, your row type. Column keys are checked against it, each column’s
value type flows into the callbacks that touch it, and TanStack Table v9’s feature-typed Table
means table.getSelectedCellCount() exists on the type because the grid registered cell selection,
not because of a global declaration.
Columns from a row type
Section titled “Columns from a row type”type Order = { id: string customer: { name: string; email: string } total: number status: "draft" | "paid" | "refunded" tags: string[] placedAt: Date}
const col = createDataGridColumns<Order>()Each builder method is generic over the accessor. A string accessor must be a DeepKeys<Order>:
"total", "customer.name", "customer.email"; a typo is a compile error. Its value type is
DeepValue<Order, Key>, so "customer.name" is a string column and "placedAt" a Date
column. A function accessor (row) => … gives its return type, and then needs an id.
col.text("customer.name", { header: "Customer" }) // TValue = stringcol.currency("total", { currency: "EUR" }) // TValue = numbercol.text((o) => `${o.customer.name} <${o.customer.email}>`, { id: "contact" })col.text("customer.phone") // error: not a key of OrderWhat the value type reaches
Section titled “What the value type reaches”With TValue known, the options that touch the value are typed against it:
| Option | Signature |
|---|---|
format |
(value: TValue, row: TData) => string |
editable.validate |
(value: TValue, row: TData) => string | null | undefined | void |
editable.parse |
(raw: string, row: TData) => TValue |
onEdit |
(edit: DataGridCellEdit<TData, TValue>) => void | Promise<void> |
options |
readonly DataGridOption<TValue>[]; for an array column (tags), DataGridOption<Item> |
href |
(value: TValue, row: TData) => string |
export |
boolean | (value: TValue, row: TData) => unknown |
cellClassName, tooltip |
(value: TValue, row: TData) => … |
col.enum("status", { options: [{ value: "draft", label: "Draft" }, { value: "paid", label: "Paid" }], // value: Order["status"] editable: { validate: (v, row) => (v === "refunded" && row.total === 0 ? "Nothing to refund" : null) }, onEdit: ({ value, previous }) => audit(previous, value), // both Order["status"]})col.date("placedAt", { format: (d) => d.toLocaleDateString() }) // d: Datecol.columns([...]) is TanStack’s columns helper: it keeps every column’s own TValue instead
of widening the array to unknown, so the array you pass to the grid is as precise as its parts.
The array’s type is ReadonlyArray<DataGridColumnDef<Order, any>>, where DataGridColumnDef is
ColumnDef<DataGridFeatures, TData, TValue>.
Feature-typed TanStack
Section titled “Feature-typed TanStack”TanStack Table v9 types its Table, Column, Row and Cell by the features you register, so a
method exists on the type only when its feature is present. The grid registers its set once:
export const dataGridFeatures = tableFeatures({ rowSortingFeature, columnFilteringFeature, globalFilteringFeature, columnFacetingFeature, rowPaginationFeature, rowSelectionFeature, columnVisibilityFeature, columnPinningFeature, columnOrderingFeature, columnSizingFeature, columnResizingFeature, rowExpandingFeature, columnGroupingFeature, rowAggregationFeature, rowPinningFeature, cellSelectionFeature, // row models, sortFns, filterFns, aggregationFns… columnMeta: metaHelper<DataGridColumnMeta>(),})export type DataGridFeatures = typeof dataGridFeaturesRather than spelling DataGridFeatures into every TanStack generic, use the pre-bound aliases:
| Alias | Is |
|---|---|
DataGridColumnDef<TData, TValue> |
ColumnDef<DataGridFeatures, TData, TValue> |
DataGridRow<TData> |
Row<DataGridFeatures, TData> |
DataGridCell<TData, TValue> |
Cell<DataGridFeatures, TData, TValue> |
DataGridColumn<TData, TValue> |
Column<DataGridFeatures, TData, TValue> |
DataGridHeader<TData, TValue> |
Header<DataGridFeatures, TData, TValue> |
DataGridCellContext<TData, TValue> |
CellContext<DataGridFeatures, TData, TValue> |
DataGridHeaderContext<TData, TValue> |
HeaderContext<DataGridFeatures, TData, TValue> |
DataGridState |
TableState<DataGridFeatures> |
DataGridTableOptions<TData> |
TableOptions<DataGridFeatures, TData> |
DataGridTableInstance<TData> / DataGridReactTable<TData> |
Table<…> / ReactTable<…> |
A custom cell or a part of your own is typed with them:
function AmountCell({ getValue, row }: DataGridCellContext<Order, number>) { return <span className={row.original.status === "refunded" ? "line-through" : ""}>{getValue()}</span>}col.currency("total", { cell: AmountCell })
function Summary({ grid }: { grid: DataGridApi<Order> }) { return <p>{grid.table.getSelectedCellCount()} cells, {grid.table.getFilteredRowModel().rows.length} rows</p>}Column meta without declaration merging
Section titled “Column meta without declaration merging”TanStack’s columnDef.meta is unknown unless you tell it otherwise. The usual way is a global
declare module "@tanstack/react-table" { interface ColumnMeta … }, which leaks into every table in
your app. The grid uses v9’s metaHelper instead: columnMeta: metaHelper<DataGridColumnMeta>() in
the feature set types meta as DataGridColumnMeta on these tables only.
The builder lifts the grid’s meta out of meta so a column reads as one flat object; anything you
put in meta yourself is merged last and typed as Partial<DataGridColumnMeta> & Record<string, unknown>:
col.text("customer.name", { header: "Customer", meta: { helpText: "From the billing contact" } })
// Read it back where you have the column:const help = column.columnDef.meta?.helpText as string | undefinedFor the grid’s own meta, prefer grid.metaFor(columnId): it is the resolved object, with kind
inferred from the data when the column did not declare one.
A column’s onEdit knows its value type. The grid’s onCellEdit sees every column, so its value is
unknown on purpose:
interface DataGridCellEdit<TData, TValue = unknown> { rowId: string; rowIndex: number; row: TData; columnId: string; value: TValue; previous: TValue}Narrow on columnId with a small predicate, and the value follows:
function editOf<K extends keyof Order>( edit: DataGridCellEdit<Order, unknown>, key: K,): edit is DataGridCellEdit<Order, Order[K]> { return edit.columnId === key}
useDataGrid<Order>({ data, columns, onCellEdit: async (edit) => { if (editOf(edit, "total")) await repriceOrder(edit.rowId, edit.value) // number else if (editOf(edit, "status")) await setStatus(edit.rowId, edit.value) // Order["status"] else await patchOrder(edit.rowId, { [edit.columnId]: edit.value }) },})For a nested key ("customer.name") the column id is the path; compare against the string and cast
once, or keep such edits in the column’s own onEdit where the type is already right.
Custom features
Section titled “Custom features”Need a TanStack feature the grid does not register, cell spanning say? The hook passes
dataGridFeatures as features and the table escape hatch deliberately omits features, so the
place to add one is your copy of data-grid-features.ts:
import { cellSpanningFeature } from "@tanstack/react-table"
export const dataGridFeatures = tableFeatures({ // …the existing features cellSpanningFeature,})DataGridFeatures is typeof dataGridFeatures, so every alias above picks the feature up:
spanRows and spanColumns become valid column options, cell.getRowSpan(), cell.getColSpan()
and cell.getIsCovered() appear on DataGridCell, and the grid’s own code keeps compiling because
it only uses what it already needed. Rendering the spans is then a small edit to the <td> in your
copy of data-grid-table.tsx (rowSpan, colSpan, skip covered cells).
Options for a feature go through table, which is Partial<Omit<DataGridTableOptions<TData>, "data" | "columns" | "features">>
and is applied last:
col.text("region", { spanRows: true })useDataGrid<Order>({ data, columns, table: { enableCellSpanning: true } })What is unknown, and why
Section titled “What is unknown, and why”onCellEdit’svalueandprevious: the grid cannot know which column fired. Narrow as above.exportTable().rowsisunknown[][]: formatted strings by default, raw values withformatted: false, whatever yourexportfunction returns otherwise.DataGridColumnMeta’s callbacks (format,href,cellClassName…) are typed withanyin the meta interface itself, because meta is where every column’s options meet. You never write them there: the builder’s options are the typed surface.DataGridColumnDef<TData, any>inUseDataGridOptions.columns: the array holds columns of different value types; each column stays precise insidecol.columns.
See also
Section titled “See also”- Columns: every builder option.
- API reference: every type to import.
- Inline editing