Capítulo 11 de 54
Column defs are the single most important piece of a table: they build the data model (what's sortable/filterable/groupable), format what's displayed, and can create header/footer groups or pure display-only columns (action buttons, checkboxes) with no underlying data at all.
createColumnHelper<TFeatures, TData>() (v9 needs both type params — use typeof features for TFeatures) returns .accessor(...), .display(...), .group(...) builders with the best type inference. If you used createTableHook, its returned createAppColumnHelper is already bound to your features type — pass only TData.accessorKey: 'firstName' or columnHelper.accessor('firstName')), a dot-path into nested objects (accessorKey: 'name.first'), or an accessor function for computed values or array-indexed data (row => row[1], needs an explicit id). Array-indexed accessorKey must be a string ('1', not 1).id or a primitive string header to derive one from — an accessor function with neither is a common source of "column id not found" bugs.ColumnDef objects at runtime from Object.keys(data[0]); the column helper offers no benefit here since there's no static shape to infer against. Two rules: rebuild the array only when data actually changes (memoize it), and since values are unknown, sample a value per key to pick a type-appropriate sortFn/filterFn/filter UI.cell (regular cells, via props.getValue(), also has props.row/props.table), aggregatedCell (cells for grouped/aggregated rows — see Aggregation Guide), header, footer. All default to stringifying the raw value if omitted.sortFn, filterFn, aggregationFn (string names typed from the registries you passed into tableFeatures, e.g. sortFns/filterFns/aggregationFns — an unregistered name is a type error), plus enable flags like enableSorting/enableColumnFilter.meta attaches arbitrary strongly-typed per-column data (e.g. a filter-variant tag or detected type) — see metaHelper in the Table and Column Meta Guide.const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.display({ id: 'actions', cell: (p) => <RowActions row={p.row} /> }),
columnHelper.group({
header: 'Name',
columns: [
columnHelper.accessor('firstName', { cell: (info) => info.getValue() }),
columnHelper.accessor((row) => row.lastName, { id: 'lastName', header: 'Last Name' }),
],
}),
columnHelper.accessor('age', { sortFn: 'basic', filterFn: 'inNumberRange' }),
])
columnHelper.columns([...]), which gives better inference for nested groups than a plain array literal.For unknown-shape data, skip the column helper entirely and build ColumnDef<typeof features, Record<string, unknown>>[] at runtime — memoized on data so it doesn't rebuild every render:
const columns = useMemo(
() => data.length
? Object.keys(data[0]).map((key) => ({
accessorKey: key,
header: formatHeader(key), // 'firstName' -> 'First Name'
cell: (info) => String(info.getValue() ?? ''),
}))
: [],
[data],
)
Combine this with per-key sampling (check typeof data[0][key]) to pick a registered sortFn/filterFn name suited to the detected type — a switch on the type is usually enough.
columnHelper over plain object literals whenever the row shape is statically known — the inference difference is significant, especially for nested groups.header or explicit id → column id resolution breaks. Always supply one of the two.ColumnDef[] directly, memoized on data.sortFn: 'basic') come from the registries wired into tableFeatures; a name TypeScript doesn't recognize means it isn't registered there.meta with metaHelper.ColumnDef variants (AccessorColumnDef, DisplayColumnDef, GroupColumnDef).sortFn/filterFn/aggregationFn name actually does.