From 23cc80a9742a9713dd5fa7948916e70a58bba0d5 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Thu, 11 Jun 2026 09:14:51 -0500 Subject: [PATCH] feat: per table table and column meta --- docs/config.json | 3 +- docs/framework/angular/guide/migrating.md | 19 +- docs/framework/lit/guide/migrating.md | 19 +- docs/framework/preact/guide/migrating.md | 19 +- docs/framework/react/guide/migrating.md | 21 +- docs/framework/solid/guide/migrating.md | 19 +- docs/framework/svelte/guide/migrating.md | 19 +- docs/framework/vue/guide/migrating.md | 19 +- docs/guide/table-and-column-meta.md | 518 ++++++++++++++++++ examples/angular/editable/src/app/app.ts | 10 +- .../angular/filters-faceted/src/app/app.ts | 7 + .../src/app/table-filter/table-filter.ts | 19 +- examples/angular/filters/src/app/app.ts | 7 + .../src/app/table-filter/table-filter.ts | 19 +- examples/angular/kitchen-sink/src/app/app.ts | 56 +- examples/lit/filters/src/main.ts | 26 +- examples/lit/kitchen-sink/src/main.ts | 79 ++- examples/preact/filters-faceted/src/main.tsx | 25 +- examples/preact/filters/src/main.tsx | 21 +- examples/preact/kitchen-sink/src/main.tsx | 52 +- examples/react/filters-faceted/src/main.tsx | 25 +- examples/react/filters/src/main.tsx | 21 +- .../react/kitchen-sink-hero-ui/src/main.tsx | 19 +- .../react/kitchen-sink-mantine/src/main.tsx | 19 +- .../kitchen-sink-material-ui/src/main.tsx | 19 +- .../kitchen-sink-react-aria/src/main.tsx | 19 +- .../data-table/data-table-filter-list.tsx | 5 +- .../kitchen-sink-shadcn-base/src/main.tsx | 19 +- .../data-table/data-table-filter-list.tsx | 5 +- .../kitchen-sink-shadcn-radix/src/main.tsx | 22 +- examples/react/kitchen-sink/src/main.tsx | 58 +- .../src/components/table.tsx | 8 + .../src/utils/userColumns.tsx | 12 - examples/solid/kitchen-sink/src/App.tsx | 40 +- .../src/components/table.tsx | 13 +- .../src/utils/userColumns.tsx | 18 +- examples/svelte/kitchen-sink/src/App.svelte | 35 +- .../kitchen-sink/src/table-augmentations.ts | 16 +- examples/vue/kitchen-sink/src/App.vue | 50 +- .../src/core/table/constructTable.ts | 9 +- .../src/core/table/coreTablesFeature.types.ts | 25 +- packages/table-core/src/helpers/metaHelper.ts | 24 + .../table-core/src/helpers/tableFeatures.ts | 12 +- packages/table-core/src/index.ts | 1 + packages/table-core/src/types/ColumnDef.ts | 30 +- .../table-core/src/types/TableFeatures.ts | 26 +- packages/table-core/src/types/TableOptions.ts | 5 +- .../unit/core/table/metaTypeSlots.test.ts | 127 +++++ 48 files changed, 1194 insertions(+), 465 deletions(-) create mode 100644 docs/guide/table-and-column-meta.md create mode 100644 packages/table-core/src/helpers/metaHelper.ts create mode 100644 packages/table-core/tests/unit/core/table/metaTypeSlots.test.ts diff --git a/docs/config.json b/docs/config.json index 16b8d2526d..8d1e624f44 100644 --- a/docs/config.json +++ b/docs/config.json @@ -84,7 +84,8 @@ { "label": "Cells", "to": "guide/cells" }, { "label": "Header Groups", "to": "guide/header-groups" }, { "label": "Headers", "to": "guide/headers" }, - { "label": "Columns", "to": "guide/columns" } + { "label": "Columns", "to": "guide/columns" }, + { "label": "Table and Column Meta", "to": "guide/table-and-column-meta" } ], "frameworks": [ { diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md index b324c1546c..2e159e1378 100644 --- a/docs/framework/angular/guide/migrating.md +++ b/docs/framework/angular/guide/migrating.md @@ -718,9 +718,22 @@ import type { StockFeatures, ColumnDef } from '@tanstack/angular-table' const columns: ColumnDef[] = [...] ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes -If you're using module augmentation to extend `ColumnMeta`, note that it now requires a `TFeatures` parameter. +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging to extend `TableMeta` or `ColumnMeta` works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ customProperty: string }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. ### `RowData` Type Restriction @@ -750,7 +763,7 @@ This change improves type safety. If you were passing unusual data types, ensure - [ ] Rename `sortingFn` → `sortFn` in column definitions - [ ] Split column sizing/resizing: use both `columnSizingFeature` and `columnResizingFeature` if needed - [ ] Rename `columnSizingInfo` state → `columnResizing` (and related options) -- [ ] Update `ColumnMeta` module augmentation to include `TFeatures` generic (if used) +- [ ] If you use `TableMeta`/`ColumnMeta` declaration merging, add the `TFeatures` generic to your augmentations (optionally, switch to the per-table `tableMeta`/`columnMeta` feature slots) - [ ] (Optional) Use `tableOptions()` for composable configurations - [ ] (Optional) Use `createTableHook` for reusable table patterns diff --git a/docs/framework/lit/guide/migrating.md b/docs/framework/lit/guide/migrating.md index dd6e6e9c48..a0e2603fa8 100644 --- a/docs/framework/lit/guide/migrating.md +++ b/docs/framework/lit/guide/migrating.md @@ -586,7 +586,11 @@ import type { StockFeatures } from '@tanstack/lit-table' type PersonColumn = ColumnDef ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes + +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```ts declare module '@tanstack/lit-table' { @@ -596,6 +600,19 @@ declare module '@tanstack/lit-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ align?: 'left' | 'right' }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction Prefer explicit object row types: diff --git a/docs/framework/preact/guide/migrating.md b/docs/framework/preact/guide/migrating.md index 5e7849fe71..33005354a9 100644 --- a/docs/framework/preact/guide/migrating.md +++ b/docs/framework/preact/guide/migrating.md @@ -573,9 +573,11 @@ import type { StockFeatures } from '@tanstack/preact-table' type PersonColumn = ColumnDef ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes -Module augmentation now includes `TFeatures`: +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```tsx declare module '@tanstack/preact-table' { @@ -585,6 +587,19 @@ declare module '@tanstack/preact-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ align?: 'left' | 'right' }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction `RowData` is now constrained to record-like objects or arrays. Prefer object row types such as: diff --git a/docs/framework/react/guide/migrating.md b/docs/framework/react/guide/migrating.md index c76af45cf7..b01d1b1d7e 100644 --- a/docs/framework/react/guide/migrating.md +++ b/docs/framework/react/guide/migrating.md @@ -988,9 +988,11 @@ import type { StockFeatures, ColumnDef } from '@tanstack/react-table' const columns: ColumnDef[] = [...] ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes -If you're using module augmentation to extend `ColumnMeta`, note that it now requires a `TFeatures` parameter: +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging to extend `TableMeta` or `ColumnMeta` works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```tsx // v8 @@ -1008,6 +1010,19 @@ declare module '@tanstack/react-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ customProperty: string }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction The `RowData` type is now more restrictive: @@ -1037,7 +1052,7 @@ This change improves type safety. If you were passing unusual data types, ensure - [ ] Rename `sortingFn` → `sortFn` in column definitions - [ ] Split column sizing/resizing: use both `columnSizingFeature` and `columnResizingFeature` if needed - [ ] Rename `columnSizingInfo` state → `columnResizing` (and related options) -- [ ] Update `ColumnMeta` module augmentation to include `TFeatures` generic (if used) +- [ ] If you use `TableMeta`/`ColumnMeta` declaration merging, add the `TFeatures` generic to your augmentations (optionally, switch to the per-table `tableMeta`/`columnMeta` feature slots) - [ ] (Optional) Add `table.Subscribe` for render optimizations - [ ] (Optional) Subscribe to individual slices via `table.atoms.` + `useSelector` for the narrowest re-renders - [ ] (Optional) Pass writable atoms via the new `atoms` option to own specific state slices externally diff --git a/docs/framework/solid/guide/migrating.md b/docs/framework/solid/guide/migrating.md index 88af9ad7fb..dcbc1c7269 100644 --- a/docs/framework/solid/guide/migrating.md +++ b/docs/framework/solid/guide/migrating.md @@ -542,7 +542,11 @@ import type { StockFeatures } from '@tanstack/solid-table' type PersonColumn = ColumnDef ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes + +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```tsx declare module '@tanstack/solid-table' { @@ -552,6 +556,19 @@ declare module '@tanstack/solid-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ align?: 'left' | 'right' }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction Prefer explicit object row types: diff --git a/docs/framework/svelte/guide/migrating.md b/docs/framework/svelte/guide/migrating.md index ecd16d55b6..eea49109d1 100644 --- a/docs/framework/svelte/guide/migrating.md +++ b/docs/framework/svelte/guide/migrating.md @@ -621,7 +621,11 @@ import type { StockFeatures } from '@tanstack/svelte-table' type PersonColumn = ColumnDef ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes + +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```ts declare module '@tanstack/svelte-table' { @@ -631,6 +635,19 @@ declare module '@tanstack/svelte-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ align?: 'left' | 'right' }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction Prefer explicit object row types: diff --git a/docs/framework/vue/guide/migrating.md b/docs/framework/vue/guide/migrating.md index dfb4fc2cc2..ed19d9ff5c 100644 --- a/docs/framework/vue/guide/migrating.md +++ b/docs/framework/vue/guide/migrating.md @@ -567,7 +567,11 @@ import type { StockFeatures } from '@tanstack/vue-table' type PersonColumn = ColumnDef ``` -### `ColumnMeta` Generic Change +### `TableMeta`/`ColumnMeta` Typing Changes + +No more declaration merging required! (Although it still works if you want to keep using it) + +Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take `TFeatures` as the first type parameter. ```ts declare module '@tanstack/vue-table' { @@ -577,6 +581,19 @@ declare module '@tanstack/vue-table' { } ``` +That's all that's required if you want to keep declaring meta types globally. + +Optionally, v9 also adds a new way to declare meta types **per-table** without declaration merging. You can use type-only `tableMeta`/`columnMeta` slots on the `features` option, which only affect tables created with that `features` object: + +```ts +const features = tableFeatures({ + rowSortingFeature, + columnMeta: metaHelper<{ align?: 'left' | 'right' }>(), +}) +``` + +See the new [Table and Column Meta Guide](../../../guide/table-and-column-meta) for full details on both approaches. + ### `RowData` Type Restriction Prefer explicit object row types: diff --git a/docs/guide/table-and-column-meta.md b/docs/guide/table-and-column-meta.md new file mode 100644 index 0000000000..73f5df2cee --- /dev/null +++ b/docs/guide/table-and-column-meta.md @@ -0,0 +1,518 @@ +--- +title: Table and Column Meta Guide +--- + +## Table and Column Meta Guide + +Sometimes you need to attach your own arbitrary data or functions to a table or its columns so that they are available anywhere the `table` or `column` instances are available. That is what the `meta` options are for. TanStack Table never reads or writes `meta` itself; it is purely a place for you to pass your own context through the table. + +There are two kinds of meta: + +- **Table meta** - The `meta` table option. Pass any object and read it back anywhere via `table.options.meta`. A classic use case is passing an `updateData` function down to editable cells. +- **Column meta** - The `meta` property on a column definition. Read it back anywhere a column is available via `column.columnDef.meta`. A classic use case is declaring which filter UI variant a column's header should render. + + + +# React + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Preact + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Vue + +```ts +const table = useTable({ + features, + rowModels: {}, + columns, + data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Solid + +```ts +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data() + }, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Svelte + +```ts +const table = createTable({ + features, + rowModels: {}, + columns, + get data() { + return data + }, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Angular + +```ts +readonly table = injectTable(() => ({ + features, + rowModels: {}, + columns, + data: this.data(), + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +})) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Lit + +```ts +const table = this.tableController.table({ + features, + rowModels: {}, + columns, + data: this.data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + +# Vanilla + +```ts +const table = constructTable({ + features, + rowModels: {}, + columns, + data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + + + +Column meta is set on the column definition and is identical across every adapter: + +```ts +const columns = columnHelper.columns([ + columnHelper.accessor('age', { + header: 'Age', + meta: { + filterVariant: 'range', + }, + }), +]) + +// ...later, anywhere a column is available (e.g. inside a header component) +const variant = column.columnDef.meta?.filterVariant +``` + +### Typing Meta Per-Table (Recommended) + +By default, both meta types are empty objects, so to get type safety you declare their shapes yourself. New in v9, you can declare meta types **per features set** with the type-only `tableMeta` and `columnMeta` slots in your `tableFeatures()` call, using the `metaHelper` utility. + +First, define the shapes you want (this is the same in every framework): + +```ts +interface MyTableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void +} + +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} +``` + +Then declare them on your `features` object: + + + +# React + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/react-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Preact + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/preact-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Vue + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/vue-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Solid + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/solid-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Svelte + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/svelte-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Angular + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/angular-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Lit + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/lit-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + +# Vanilla + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/table-core' +import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings' + +const features = tableFeatures({ + coreReactivityFeature: storeReactivityBindings(), + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + + + +That's it. Everywhere this `features` object flows (`useTable`, `createColumnHelper`, `ColumnDef`, `Column`, and so on), the meta types are inferred from `typeof features` with no extra generics to pass around: + +```ts +const columnHelper = createColumnHelper() + +columnHelper.accessor('age', { + meta: { + filterVariant: 'range', // ✅ type-checked against MyColumnMeta + }, +}) + +// And both meta surfaces are fully typed wherever you read them: +table.options.meta?.updateData // ✅ (rowIndex, columnId, value) => void +column.columnDef.meta?.filterVariant // ✅ 'text' | 'range' | 'select' | undefined +``` + +Unlike the v8-style declaration merging described below, this scoping is **per-table, not global**: only tables created with this `features` object get these meta types. Different tables in your app can declare entirely different meta shapes by using different `features` objects. + +#### How the Type-Only Slots Work + +The `tableMeta` and `columnMeta` keys are *phantom* entries: only their TypeScript types matter. At runtime, the value is an empty object that gets stripped from the table's registered features, so it is never treated as a real feature. The actual meta *values* are still passed where they always were: the `meta` table option and the `meta` property on column definitions. + +`metaHelper()` simply returns `{}` cast to your meta type. You can write the cast yourself instead: + +```ts +const features = tableFeatures({ + rowSortingFeature, + tableMeta: {} as MyTableMeta, + columnMeta: {} as MyColumnMeta, +}) +``` + +Both forms are equivalent. Prefer `metaHelper`: it reads as type-only at a glance, and it avoids false positives from the `@typescript-eslint/no-unnecessary-type-assertion` lint rule, which flags the `{} as` form when your meta type has only optional properties (and whose auto-fix would silently erase your meta type). + +### Typing Meta Globally with Declaration Merging (v8 Style) + +The v8 approach of extending the `TableMeta` and `ColumnMeta` interfaces with module augmentation still works in v9. The only change from v8 is the generics shape: `TFeatures` is now the first type parameter on both interfaces. + + + +# React + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/react-table' + +declare module '@tanstack/react-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Preact + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/preact-table' + +declare module '@tanstack/preact-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Vue + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/vue-table' + +declare module '@tanstack/vue-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Solid + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/solid-table' + +declare module '@tanstack/solid-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Svelte + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/svelte-table' + +declare module '@tanstack/svelte-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Angular + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/angular-table' + +declare module '@tanstack/angular-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Lit + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/lit-table' + +declare module '@tanstack/lit-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + +# Vanilla + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/table-core' + +declare module '@tanstack/table-core' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + + + +The trade-off with declaration merging is that it is **global**. Every table in your entire project gets the same meta types, whether or not a given table actually provides that meta. If you have many tables with different needs, the per-table slots above are a better fit. + +The two approaches resolve with a simple precedence: if a `features` object declares a `tableMeta` or `columnMeta` slot, that slot's type is used for tables created with those features, *replacing* (not merging with) the globally declared interface. Tables whose features declare no slot fall back to the declaration-merged interfaces. + +### When to Use Custom Features Instead + +Meta is intentionally simple: a typed bag of values you carry through the table. If you find yourself wanting real table *options* with defaults, new *state*, or new *APIs* on the table instance (e.g. `table.toggleDensity()`), consider writing a [custom feature](../framework/react/guide/custom-features) instead. Custom features plug into the same `features` option, get the same `typeof features` type inference, and can declare their own options, state, and instance methods. Meta was never designed to do any of that. diff --git a/examples/angular/editable/src/app/app.ts b/examples/angular/editable/src/app/app.ts index 09c736e7ae..945b664482 100644 --- a/examples/angular/editable/src/app/app.ts +++ b/examples/angular/editable/src/app/app.ts @@ -11,22 +11,22 @@ import { createPaginatedRowModel, flexRenderComponent, injectTable, + metaHelper, rowPaginationFeature, tableFeatures, } from '@tanstack/angular-table' import { EditableCell } from './editable-cell/editable-cell' import { makeData } from './makeData' import type { Person } from './makeData' -import type { ColumnDef, RowData, TableFeatures } from '@tanstack/angular-table' +import type { ColumnDef } from '@tanstack/angular-table' -declare module '@tanstack/angular-table' { - interface TableMeta { - updateData: (rowIndex: number, columnId: string, value: unknown) => void - } +interface MyTableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void } const features = tableFeatures({ rowPaginationFeature, + tableMeta: metaHelper(), }) const defaultColumn: Partial> = { diff --git a/examples/angular/filters-faceted/src/app/app.ts b/examples/angular/filters-faceted/src/app/app.ts index fd2e986a01..5c1e7e5607 100644 --- a/examples/angular/filters-faceted/src/app/app.ts +++ b/examples/angular/filters-faceted/src/app/app.ts @@ -11,6 +11,7 @@ import { createTableHook, filterFns, isFunction, + metaHelper, rowPaginationFeature, tableFeatures, } from '@tanstack/angular-table' @@ -19,10 +20,16 @@ import { TableFilter } from './table-filter/table-filter' import type { ColumnFiltersState, Updater } from '@tanstack/angular-table' import type { Person } from './makeData' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + export const features = tableFeatures({ columnFilteringFeature, columnFacetingFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const { injectAppTable, createAppColumnHelper } = createTableHook({ diff --git a/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts b/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts index bea5658bc2..83b9ab0b1a 100644 --- a/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts +++ b/examples/angular/filters-faceted/src/app/table-filter/table-filter.ts @@ -2,24 +2,7 @@ import { Component, computed, input } from '@angular/core' import { DebouncedInput } from '../debounced-input/debounced-input' import type { features } from '../app' import type { Person } from '../makeData' -import type { - CellData, - Column, - RowData, - Table, - TableFeatures, -} from '@tanstack/angular-table' - -declare module '@tanstack/angular-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } -} +import type { Column, Table } from '@tanstack/angular-table' @Component({ selector: 'app-table-filter', diff --git a/examples/angular/filters/src/app/app.ts b/examples/angular/filters/src/app/app.ts index 21e983cd2a..4f3f014511 100644 --- a/examples/angular/filters/src/app/app.ts +++ b/examples/angular/filters/src/app/app.ts @@ -7,6 +7,7 @@ import { createTableHook, filterFns, isFunction, + metaHelper, rowPaginationFeature, tableFeatures, } from '@tanstack/angular-table' @@ -15,9 +16,15 @@ import { TableFilter } from './table-filter/table-filter' import type { ColumnFiltersState, Updater } from '@tanstack/angular-table' import type { Person } from './makeData' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + export const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const { injectAppTable, createAppColumnHelper } = createTableHook({ diff --git a/examples/angular/filters/src/app/table-filter/table-filter.ts b/examples/angular/filters/src/app/table-filter/table-filter.ts index 6bb23a05b3..8c5b503db7 100644 --- a/examples/angular/filters/src/app/table-filter/table-filter.ts +++ b/examples/angular/filters/src/app/table-filter/table-filter.ts @@ -2,24 +2,7 @@ import { Component, computed, input } from '@angular/core' import { DebouncedInput } from '../debounced-input/debounced-input' import type { features } from '../app' import type { Person } from '../makeData' -import type { - CellData, - Column, - RowData, - Table, - TableFeatures, -} from '@tanstack/angular-table' - -declare module '@tanstack/angular-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } -} +import type { Column, Table } from '@tanstack/angular-table' @Component({ selector: 'app-table-filter', diff --git a/examples/angular/kitchen-sink/src/app/app.ts b/examples/angular/kitchen-sink/src/app/app.ts index a097a1ca45..eaed63668d 100644 --- a/examples/angular/kitchen-sink/src/app/app.ts +++ b/examples/angular/kitchen-sink/src/app/app.ts @@ -21,8 +21,10 @@ import { createSortedRowModel, filterFns, injectTable, + metaHelper, sortFns, stockFeatures, + tableFeatures, } from '@tanstack/angular-table' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' import { DebouncedInput } from './debounced-input/debounced-input' @@ -33,7 +35,6 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, ColumnDef, FilterFn, @@ -41,28 +42,28 @@ import type { Row, RowData, SortFn, - TableFeatures, } from '@tanstack/angular-table' -export const features = stockFeatures +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +export const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) declare module '@tanstack/angular-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { - fuzzy: FilterFn + fuzzy: FilterFn } interface FilterMeta { itemRank?: RankingInfo } } -const fuzzyFilter: FilterFn = ( +const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -73,11 +74,7 @@ const fuzzyFilter: FilterFn = ( return itemRank.passed } -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 if (rowA.columnFiltersMeta[columnId]) { dir = compareItems( @@ -88,7 +85,7 @@ const fuzzySort: SortFn = ( return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -96,10 +93,10 @@ const sortStatusFn: SortFn = (rowA, rowB) => { ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() -const columns: Array> = - columnHelper.columns([ +const columns: Array> = columnHelper.columns( + [ columnHelper.display({ id: 'select', size: 80, @@ -161,7 +158,8 @@ const columns: Array> = aggregatedCell: ({ getValue }) => `${Math.round(getValue() * 100) / 100}%`, }), - ]) + ], +) @Component({ selector: 'app-root', @@ -179,8 +177,8 @@ const columns: Array> = export class App { readonly data = signal(makeData(1_000)) - readonly table = injectTable(() => ({ - features: stockFeatures, + readonly table = injectTable(() => ({ + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ @@ -260,7 +258,7 @@ export class App { this.table.setPageSize(Number((event.target as HTMLSelectElement).value)) } - getCommonPinningStyles(column: Column) { + getCommonPinningStyles(column: Column) { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left') @@ -281,18 +279,18 @@ export class App { } } - headerStyles(header: Header) { + headerStyles(header: Header) { return { ...this.getCommonPinningStyles(header.column), whiteSpace: 'nowrap', } } - cellStyles(cell: Cell) { + cellStyles(cell: Cell) { return this.getCommonPinningStyles(cell.column) } - cellClass(cell: Cell) { + cellClass(cell: Cell) { const groupingActive = this.table.atoms.grouping.get().length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn return !groupingActive @@ -306,7 +304,7 @@ export class App { : undefined } - pinnedRowStyles(row: Row) { + pinnedRowStyles(row: Row) { const bottomRows = this.table.getBottomRows() return { position: 'sticky', diff --git a/examples/lit/filters/src/main.ts b/examples/lit/filters/src/main.ts index ead6c3a6ce..cc0644ab27 100644 --- a/examples/lit/filters/src/main.ts +++ b/examples/lit/filters/src/main.ts @@ -3,6 +3,7 @@ import { LitElement, html } from 'lit' import { repeat } from 'lit/directives/repeat.js' import { FlexRender, + metaHelper, TableController, columnFilteringFeature, createFilteredRowModel, @@ -12,18 +13,18 @@ import { tableFeatures, } from '@tanstack/lit-table' import { makeData } from './makeData' -import type { - CellData, - Column, - ColumnDef, - RowData, - TableFeatures, -} from '@tanstack/lit-table' +import type { Column, ColumnDef } from '@tanstack/lit-table' import type { Person } from './makeData' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const columns: Array> = [ @@ -73,17 +74,6 @@ const columns: Array> = [ }, ] -declare module '@tanstack/lit-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } -} - @customElement('column-filter') class ColumnFilter extends LitElement { @property() diff --git a/examples/lit/kitchen-sink/src/main.ts b/examples/lit/kitchen-sink/src/main.ts index 195b5e247c..a436b216ad 100644 --- a/examples/lit/kitchen-sink/src/main.ts +++ b/examples/lit/kitchen-sink/src/main.ts @@ -5,6 +5,7 @@ import { styleMap } from 'lit/directives/style-map.js' import { faker } from '@faker-js/faker' import { FlexRender, + metaHelper, TableController, aggregationFns, createColumnHelper, @@ -19,6 +20,7 @@ import { filterFns, sortFns, stockFeatures, + tableFeatures, } from '@tanstack/lit-table' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' import { makeData } from './makeData' @@ -26,7 +28,6 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, ColumnDef, FilterFn, @@ -35,26 +36,27 @@ import type { Row, RowData, SortFn, - TableFeatures, } from '@tanstack/lit-table' declare module '@tanstack/lit-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { - fuzzy: FilterFn + fuzzy: FilterFn } interface FilterMeta { itemRank?: RankingInfo } } -const fuzzyFilter: FilterFn = ( +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) + +const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -65,11 +67,7 @@ const fuzzyFilter: FilterFn = ( return itemRank.passed } -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 if (rowA.columnFiltersMeta[columnId]) { dir = compareItems( @@ -80,7 +78,7 @@ const fuzzySort: SortFn = ( return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -88,10 +86,10 @@ const sortStatusFn: SortFn = (rowA, rowB) => { ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() -const columns: Array> = - columnHelper.columns([ +const columns: Array> = columnHelper.columns( + [ columnHelper.display({ id: 'select', size: 80, @@ -153,23 +151,22 @@ const columns: Array> = aggregatedCell: ({ getValue }) => `${Math.round(getValue() * 100) / 100}%`, }), - ]) + ], +) @customElement('lit-table-example') class LitTableExample extends LitElement { @state() private _data: Array = makeData(1_000) - private tableController = new TableController( - this, - ) + private tableController = new TableController(this) private debounceTimers = new Map>() protected render() { const table = this.tableController.table( { - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ @@ -418,8 +415,8 @@ class LitTableExample extends LitElement { } private renderHeader( - table: LitTable, - header: Header, + table: LitTable, + header: Header, ) { const column = header.column return html` @@ -501,7 +498,7 @@ class LitTableExample extends LitElement { ` } - private renderFilter(column: Column) { + private renderFilter(column: Column) { const filterVariant = column.columnDef.meta?.filterVariant const columnFilterValue = column.getFilterValue() const minMaxValues = @@ -592,8 +589,8 @@ class LitTableExample extends LitElement { } private renderPinnedRow( - table: LitTable, - row: Row, + table: LitTable, + row: Row, ) { return html` @@ -603,8 +600,8 @@ class LitTableExample extends LitElement { } private renderCell( - table: LitTable, - cell: Cell, + table: LitTable, + cell: Cell, ) { return html` ) { + private shuffleColumns(table: LitTable) { table.setColumnOrder( faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), ) } - private getCommonPinningStyle(column: Column) { + private getCommonPinningStyle(column: Column) { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left') @@ -696,7 +693,7 @@ class LitTableExample extends LitElement { } } - private headerStyle(header: Header) { + private headerStyle(header: Header) { return { ...this.getCommonPinningStyle(header.column), whiteSpace: 'nowrap', @@ -704,7 +701,7 @@ class LitTableExample extends LitElement { } } - private cellStyle(cell: Cell) { + private cellStyle(cell: Cell) { return { ...this.getCommonPinningStyle(cell.column), width: `calc(var(--col-${cell.column.id}-size) * 1px)`, @@ -712,8 +709,8 @@ class LitTableExample extends LitElement { } private pinnedRowStyle( - table: LitTable, - row: Row, + table: LitTable, + row: Row, ) { const bottomRows = table.getBottomRows() return { @@ -731,8 +728,8 @@ class LitTableExample extends LitElement { } private cellClass( - table: LitTable, - cell: Cell, + table: LitTable, + cell: Cell, ) { const groupingActive = table.state.grouping.length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn @@ -747,7 +744,7 @@ class LitTableExample extends LitElement { : undefined } - private tableStyle(table: LitTable) { + private tableStyle(table: LitTable) { const styles = [`width: ${table.getTotalSize()}px`] for (const header of table.getFlatHeaders()) { styles.push(`--header-${header.id}-size: ${header.getSize()}`) diff --git a/examples/preact/filters-faceted/src/main.tsx b/examples/preact/filters-faceted/src/main.tsx index 9e8bae5ff0..2b99dcfa41 100644 --- a/examples/preact/filters-faceted/src/main.tsx +++ b/examples/preact/filters-faceted/src/main.tsx @@ -11,6 +11,7 @@ import { createFilteredRowModel, createPaginatedRowModel, filterFns, + metaHelper, rowPaginationFeature, tableFeatures, useTable, @@ -18,33 +19,23 @@ import { import { useDebouncedCallback } from '@tanstack/preact-pacer/debouncer' import { makeData } from './makeData' import type { JSX } from 'preact' -import type { - CellData, - Column, - RowData, - TableFeatures, -} from '@tanstack/preact-table' +import type { Column } from '@tanstack/preact-table' import type { Person } from './makeData' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + const features = tableFeatures({ columnFacetingFeature, columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() -declare module '@tanstack/preact-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } -} - function App() { const columns = useMemo( () => diff --git a/examples/preact/filters/src/main.tsx b/examples/preact/filters/src/main.tsx index e617c0493d..c3a7473f91 100644 --- a/examples/preact/filters/src/main.tsx +++ b/examples/preact/filters/src/main.tsx @@ -7,6 +7,7 @@ import { createFilteredRowModel, createPaginatedRowModel, filterFns, + metaHelper, rowPaginationFeature, tableFeatures, useTable, @@ -14,28 +15,18 @@ import { import { useDebouncedCallback } from '@tanstack/preact-pacer/debouncer' import { makeData } from './makeData' import type { JSX } from 'preact' -import type { - CellData, - Column, - RowData, - TableFeatures, -} from '@tanstack/preact-table' +import type { Column } from '@tanstack/preact-table' import type { Person } from './makeData' -declare module '@tanstack/preact-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' } const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/preact/kitchen-sink/src/main.tsx b/examples/preact/kitchen-sink/src/main.tsx index 7836989c7f..e5a9ff1951 100644 --- a/examples/preact/kitchen-sink/src/main.tsx +++ b/examples/preact/kitchen-sink/src/main.tsx @@ -14,8 +14,10 @@ import { createPaginatedRowModel, createSortedRowModel, filterFns, + metaHelper, sortFns, stockFeatures, + tableFeatures, useTable, } from '@tanstack/preact-table' import { @@ -29,7 +31,6 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, FilterFn, Header, @@ -37,27 +38,28 @@ import type { Row, RowData, SortFn, - TableFeatures, } from '@tanstack/preact-table' import './index.css' declare module '@tanstack/preact-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { - fuzzy: FilterFn + fuzzy: FilterFn } interface FilterMeta { itemRank?: RankingInfo } } -const fuzzyFilter: FilterFn = ( +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) + +const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -68,11 +70,7 @@ const fuzzyFilter: FilterFn = ( return itemRank.passed } -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 if (rowA.columnFiltersMeta[columnId]) { dir = compareItems( @@ -83,7 +81,7 @@ const fuzzySort: SortFn = ( return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -92,7 +90,7 @@ const sortStatusFn: SortFn = (rowA, rowB) => { } const getCommonPinningStyles = ( - column: Column, + column: Column, ): JSX.CSSProperties => { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = @@ -169,7 +167,7 @@ function DebouncedInput({ ) } -function Filter({ column }: { column: Column }) { +function Filter({ column }: { column: Column }) { const { filterVariant } = column.columnDef.meta ?? {} const columnFilterValue = column.getFilterValue() const minMaxValues = @@ -251,8 +249,8 @@ function TableHeader({ header, table, }: { - header: Header - table: PreactTable + header: Header + table: PreactTable }) { const column = header.column const style: JSX.CSSProperties = { @@ -354,8 +352,8 @@ function TableCell({ cell, table, }: { - cell: Cell - table: PreactTable + cell: Cell + table: PreactTable }) { const groupingActive = table.state.grouping.length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn @@ -397,8 +395,8 @@ function PinnedRow({ row, table, }: { - row: Row - table: PreactTable + row: Row + table: PreactTable }) { const bottomRows = table.getBottomRows() return ( @@ -428,7 +426,7 @@ function App() { const rerender = useReducer(() => ({}), {})[1] const columns = useMemo(() => { - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper() return columnHelper.columns([ columnHelper.display({ id: 'select', @@ -544,7 +542,7 @@ function App() { const table = useTable( { key: 'kitchen-sink', // needed for devtools - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ diff --git a/examples/react/filters-faceted/src/main.tsx b/examples/react/filters-faceted/src/main.tsx index 6726155ec0..d99cca0402 100644 --- a/examples/react/filters-faceted/src/main.tsx +++ b/examples/react/filters-faceted/src/main.tsx @@ -11,39 +11,30 @@ import { createFilteredRowModel, createPaginatedRowModel, filterFns, + metaHelper, rowPaginationFeature, tableFeatures, useTable, } from '@tanstack/react-table' import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' import { makeData } from './makeData' -import type { - CellData, - Column, - RowData, - TableFeatures, -} from '@tanstack/react-table' +import type { Column } from '@tanstack/react-table' import type { Person } from './makeData' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + const features = tableFeatures({ columnFacetingFeature, columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() -declare module '@tanstack/react-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } -} - function App() { const columns = React.useMemo( () => diff --git a/examples/react/filters/src/main.tsx b/examples/react/filters/src/main.tsx index ffdd7187b5..439ffbf024 100644 --- a/examples/react/filters/src/main.tsx +++ b/examples/react/filters/src/main.tsx @@ -7,34 +7,25 @@ import { createFilteredRowModel, createPaginatedRowModel, filterFns, + metaHelper, rowPaginationFeature, tableFeatures, useTable, } from '@tanstack/react-table' import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' import { makeData } from './makeData' -import type { - CellData, - Column, - RowData, - TableFeatures, -} from '@tanstack/react-table' +import type { Column } from '@tanstack/react-table' import type { Person } from './makeData' -declare module '@tanstack/react-table' { - // allows us to define custom properties for our columns - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' } const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-hero-ui/src/main.tsx b/examples/react/kitchen-sink-hero-ui/src/main.tsx index e2633f2df5..1b0c54bfa7 100644 --- a/examples/react/kitchen-sink-hero-ui/src/main.tsx +++ b/examples/react/kitchen-sink-hero-ui/src/main.tsx @@ -59,6 +59,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -75,7 +76,6 @@ import type { DragEndEvent } from '@dnd-kit/core' import type { Key } from '@heroui/react' import type { Person } from '@/lib/make-data' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, @@ -83,9 +83,7 @@ import type { GroupingState, Header, ReactTable, - RowData, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' @@ -97,16 +95,10 @@ import { import { departments, makeData, statuses } from '@/lib/make-data' import './styles/globals.css' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -123,6 +115,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-mantine/src/main.tsx b/examples/react/kitchen-sink-mantine/src/main.tsx index 98e87c4084..5abc00da6e 100644 --- a/examples/react/kitchen-sink-mantine/src/main.tsx +++ b/examples/react/kitchen-sink-mantine/src/main.tsx @@ -93,6 +93,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -108,7 +109,6 @@ import { import type { Person } from '@/lib/make-data' import type { DragEndEvent } from '@dnd-kit/core' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, @@ -116,9 +116,7 @@ import type { GroupingState, Header, ReactTable, - RowData, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' @@ -130,16 +128,10 @@ import { import { departments, makeData, statuses } from '@/lib/make-data' import './styles/globals.css' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -156,6 +148,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-material-ui/src/main.tsx b/examples/react/kitchen-sink-material-ui/src/main.tsx index 492728fd12..3721e24851 100644 --- a/examples/react/kitchen-sink-material-ui/src/main.tsx +++ b/examples/react/kitchen-sink-material-ui/src/main.tsx @@ -101,6 +101,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -113,7 +114,6 @@ import { useTanStackTableDevtools } from '@tanstack/react-table-devtools' import type { Person } from '@/lib/make-data' import type { DragEndEvent } from '@dnd-kit/core' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, @@ -121,9 +121,7 @@ import type { GroupingState, Header, ReactTable, - RowData, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' @@ -135,16 +133,10 @@ import { import { departments, makeData, statuses } from '@/lib/make-data' import './styles/globals.css' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -161,6 +153,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-react-aria/src/main.tsx b/examples/react/kitchen-sink-react-aria/src/main.tsx index 1b1f6f7f36..f3a660ec36 100644 --- a/examples/react/kitchen-sink-react-aria/src/main.tsx +++ b/examples/react/kitchen-sink-react-aria/src/main.tsx @@ -65,6 +65,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -81,7 +82,6 @@ import type { DragEndEvent } from '@dnd-kit/core' import type { Key } from 'react-aria-components' import type { Person } from '@/lib/make-data' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, @@ -89,10 +89,8 @@ import type { GroupingState, Header, ReactTable, - RowData, RowSelectionState, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' @@ -104,16 +102,10 @@ import { import { departments, makeData, statuses } from '@/lib/make-data' import './styles/globals.css' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -130,6 +122,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-filter-list.tsx b/examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-filter-list.tsx index bc37ce1154..e0a0a12eff 100644 --- a/examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-filter-list.tsx +++ b/examples/react/kitchen-sink-shadcn-base/src/components/data-table/data-table-filter-list.tsx @@ -13,7 +13,6 @@ import type { ExtendedColumnFilter, FilterOperator } from '@/types' import type { CellData, Column, - ColumnMeta, ReactTable, RowData, } from '@tanstack/react-table' @@ -129,9 +128,7 @@ export function DataTableFilterList({ ) const getColumnFilterVariant = React.useCallback( - ( - column: Column, - ): ColumnMeta['variant'] => { + (column: Column) => { if (column.columnDef.meta?.variant) { return column.columnDef.meta.variant } diff --git a/examples/react/kitchen-sink-shadcn-base/src/main.tsx b/examples/react/kitchen-sink-shadcn-base/src/main.tsx index 5345ab1c6d..e27552d642 100644 --- a/examples/react/kitchen-sink-shadcn-base/src/main.tsx +++ b/examples/react/kitchen-sink-shadcn-base/src/main.tsx @@ -41,6 +41,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -55,15 +56,12 @@ import { } from '@tanstack/react-table-devtools' import type { Person } from '@/lib/make-data' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, ExpandedState, GroupingState, - RowData, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' import { Button } from '@/components/ui/button' @@ -99,16 +97,10 @@ import { ThemeProvider } from '@/components/theme-provider' import { ModeToggle } from '@/components/mode-toggle' import { Input } from '@/components/ui/input' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -125,6 +117,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-filter-list.tsx b/examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-filter-list.tsx index a637361d2c..5f1c30d2ba 100644 --- a/examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-filter-list.tsx +++ b/examples/react/kitchen-sink-shadcn-radix/src/components/data-table/data-table-filter-list.tsx @@ -17,7 +17,6 @@ import type { import type { CellData, Column, - ColumnMeta, ReactTable, RowData, } from '@tanstack/react-table' @@ -133,9 +132,7 @@ export function DataTableFilterList({ ) const getColumnFilterVariant = React.useCallback( - ( - column: Column, - ): ColumnMeta['variant'] => { + (column: Column) => { if (column.columnDef.meta?.variant) { return column.columnDef.meta.variant } diff --git a/examples/react/kitchen-sink-shadcn-radix/src/main.tsx b/examples/react/kitchen-sink-shadcn-radix/src/main.tsx index 25a1dac110..bc4cb6b720 100644 --- a/examples/react/kitchen-sink-shadcn-radix/src/main.tsx +++ b/examples/react/kitchen-sink-shadcn-radix/src/main.tsx @@ -5,7 +5,6 @@ import { TanStackDevtools } from '@tanstack/react-devtools' import * as ReactDOM from 'react-dom/client' import './index.css' import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer' - import { CheckCircle, ChevronDown, @@ -41,6 +40,7 @@ import { createSortedRowModel, filterFns, globalFilteringFeature, + metaHelper, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, @@ -55,15 +55,12 @@ import { } from '@tanstack/react-table-devtools' import type { Person } from '@/lib/make-data' import type { - CellData, Column, ColumnPinningState, ColumnSizingState, ExpandedState, GroupingState, - RowData, SortingState, - TableFeatures, } from '@tanstack/react-table' import type { ExtendedColumnFilter } from '@/types' import { Button } from '@/components/ui/button' @@ -76,12 +73,10 @@ import { TableHeader, TableRow, } from '@/components/ui/table' - import { departments, makeData, statuses } from '@/lib/make-data' import { DataTablePagination } from '@/components/data-table/data-table-pagination' import { DataTableViewOptions } from '@/components/data-table/data-table-view-options' import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header' - import { Badge } from '@/components/ui/badge' import { DropdownMenu, @@ -99,16 +94,10 @@ import { ThemeProvider } from '@/components/theme-provider' import { ModeToggle } from '@/components/mode-toggle' import { Input } from '@/components/ui/input' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - label?: string - variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' - options?: Array<{ label: string; value: string; count?: number }> - } +interface MyColumnMeta { + label?: string + variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select' + options?: Array<{ label: string; value: string; count?: number }> } export const features = tableFeatures({ @@ -125,6 +114,7 @@ export const features = tableFeatures({ columnPinningFeature, columnGroupingFeature, globalFilteringFeature, + columnMeta: metaHelper(), }) const columnHelper = createColumnHelper() diff --git a/examples/react/kitchen-sink/src/main.tsx b/examples/react/kitchen-sink/src/main.tsx index 9fbb4eae1c..b84c313b28 100644 --- a/examples/react/kitchen-sink/src/main.tsx +++ b/examples/react/kitchen-sink/src/main.tsx @@ -14,8 +14,10 @@ import { createPaginatedRowModel, createSortedRowModel, filterFns, + metaHelper, sortFns, stockFeatures, + tableFeatures, useTable, } from '@tanstack/react-table' import { @@ -48,7 +50,6 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, FilterFn, Header, @@ -56,7 +57,6 @@ import type { Row, RowData, SortFn, - TableFeatures, } from '@tanstack/react-table' import './index.css' @@ -65,27 +65,33 @@ import './index.css' // ===================================================================== declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - /** Per-column filter UI input variant. */ - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { - fuzzy: FilterFn + fuzzy: FilterFn } interface FilterMeta { itemRank?: RankingInfo } } +// ===================================================================== +// Features (with type-only column meta slot) +// ===================================================================== + +interface MyColumnMeta { + /** Per-column filter UI input variant. */ + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) + // ===================================================================== // Custom fuzzy filter / sort (from filters-fuzzy example) // ===================================================================== -const fuzzyFilter: FilterFn = ( +const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -96,11 +102,7 @@ const fuzzyFilter: FilterFn = ( return itemRank.passed } -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (rowA.columnFiltersMeta[columnId]) { @@ -116,7 +118,7 @@ const fuzzySort: SortFn = ( // Custom status sort (from sorting example) // ===================================================================== -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -129,7 +131,7 @@ const sortStatusFn: SortFn = (rowA, rowB) => { // ===================================================================== const getCommonPinningStyles = ( - column: Column, + column: Column, ): CSSProperties => { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = @@ -202,7 +204,7 @@ function DebouncedInput({ ) } -function Filter({ column }: { column: Column }) { +function Filter({ column }: { column: Column }) { const { filterVariant } = column.columnDef.meta ?? {} const columnFilterValue = column.getFilterValue() const minMaxValues = @@ -285,8 +287,8 @@ function DraggableTableHeader({ header, table, }: { - header: Header - table: ReactTable + header: Header + table: ReactTable }) { const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({ id: header.column.id }) @@ -430,8 +432,8 @@ function DragAlongCell({ cell, table, }: { - cell: Cell - table: ReactTable + cell: Cell + table: ReactTable }) { const { isDragging, setNodeRef, transform } = useSortable({ id: cell.column.id, @@ -494,8 +496,8 @@ function PinnedRow({ row, table, }: { - row: Row - table: ReactTable + row: Row + table: ReactTable }) { const bottomRows = table.getBottomRows() return ( @@ -535,7 +537,7 @@ function App() { const rerender = React.useReducer(() => ({}), {})[1] const columns = React.useMemo(() => { - const columnHelper = createColumnHelper() + const columnHelper = createColumnHelper() return columnHelper.columns([ columnHelper.display({ id: 'select', @@ -670,7 +672,7 @@ function App() { const table = useTable( { key: 'kitchen-sink', // needed for devtools - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ diff --git a/examples/react/with-tanstack-router/src/components/table.tsx b/examples/react/with-tanstack-router/src/components/table.tsx index 645b86227b..46a8cf9326 100644 --- a/examples/react/with-tanstack-router/src/components/table.tsx +++ b/examples/react/with-tanstack-router/src/components/table.tsx @@ -1,5 +1,6 @@ import { columnFilteringFeature, + metaHelper, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, @@ -17,11 +18,18 @@ import type { } from '@tanstack/react-table' import type { Filters } from '../api/types' +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterKey?: string + filterVariant?: 'text' | 'number' +} + export const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, + columnMeta: metaHelper(), }) export const DEFAULT_PAGE_INDEX = 0 diff --git a/examples/react/with-tanstack-router/src/utils/userColumns.tsx b/examples/react/with-tanstack-router/src/utils/userColumns.tsx index c6ce1ab13c..678d5943f0 100644 --- a/examples/react/with-tanstack-router/src/utils/userColumns.tsx +++ b/examples/react/with-tanstack-router/src/utils/userColumns.tsx @@ -1,19 +1,7 @@ import { createColumnHelper } from '@tanstack/react-table' -import type { CellData, RowData, TableFeatures } from '@tanstack/react-table' import type { User } from '../api/user' import type { features } from '../components/table' -declare module '@tanstack/react-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterKey?: keyof TData - filterVariant?: 'text' | 'number' - } -} - const columnHelper = createColumnHelper() export const USER_COLUMNS = columnHelper.columns([ diff --git a/examples/solid/kitchen-sink/src/App.tsx b/examples/solid/kitchen-sink/src/App.tsx index a9bd717413..7b7144203b 100644 --- a/examples/solid/kitchen-sink/src/App.tsx +++ b/examples/solid/kitchen-sink/src/App.tsx @@ -11,8 +11,10 @@ import { createSortedRowModel, createTableHook, filterFns, + metaHelper, sortFns, stockFeatures, + tableFeatures, } from '@tanstack/solid-table' import { useTanStackTableDevtools } from '@tanstack/solid-table-devtools' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' @@ -29,24 +31,15 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, FilterFn, Header, Row, RowData, SortFn, - TableFeatures, } from '@tanstack/solid-table' declare module '@tanstack/solid-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { fuzzy: FilterFn } @@ -55,8 +48,17 @@ declare module '@tanstack/solid-table' { } } +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) + const { createAppTable, createAppColumnHelper } = createTableHook({ - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ @@ -76,11 +78,7 @@ const { createAppTable, createAppColumnHelper } = createTableHook({ }, }) -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 if (rowA.columnFiltersMeta[columnId]) { dir = compareItems( @@ -91,7 +89,7 @@ const fuzzySort: SortFn = ( return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -100,7 +98,7 @@ const sortStatusFn: SortFn = (rowA, rowB) => { } const getCommonPinningStyles = ( - column: Column, + column: Column, ): JSX.CSSProperties => { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = @@ -168,7 +166,7 @@ function DebouncedInput( ) } -function Filter(props: { column: Column }) { +function Filter(props: { column: Column }) { const filterVariant = () => props.column.columnDef.meta?.filterVariant const sortedUniqueValues = createMemo(() => filterVariant() === 'range' @@ -260,7 +258,7 @@ function Filter(props: { column: Column }) { type AppTable = ReturnType> function TableHeader(props: { - header: Header + header: Header table: AppTable }) { const column = () => props.header.column @@ -352,7 +350,7 @@ function TableHeader(props: { } function TableCell(props: { - cell: Cell + cell: Cell table: AppTable }) { const className = () => { @@ -396,7 +394,7 @@ function TableCell(props: { } function PinnedRow(props: { - row: Row + row: Row table: AppTable }) { const bottomRows = () => props.table.getBottomRows() diff --git a/examples/solid/with-tanstack-router/src/components/table.tsx b/examples/solid/with-tanstack-router/src/components/table.tsx index e2e847d9d2..c662d4783d 100644 --- a/examples/solid/with-tanstack-router/src/components/table.tsx +++ b/examples/solid/with-tanstack-router/src/components/table.tsx @@ -1,6 +1,7 @@ import { columnFilteringFeature, createTable, + metaHelper, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, @@ -17,11 +18,17 @@ import type { } from '@tanstack/solid-table' import type { Filters } from '../api/types' +interface MyColumnMeta { + filterKey?: string + filterVariant?: 'text' | 'number' +} + export const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, + columnMeta: metaHelper(), }) export const DEFAULT_PAGE_INDEX = 0 @@ -131,9 +138,9 @@ export default function Table>( : 'text' } value={ - (props.filters[fieldMeta!.filterKey!] as - | string - | number) ?? '' + (props.filters[ + fieldMeta!.filterKey! as keyof T + ] as string | number) ?? '' } /> diff --git a/examples/solid/with-tanstack-router/src/utils/userColumns.tsx b/examples/solid/with-tanstack-router/src/utils/userColumns.tsx index c5a37b4544..9eae8e0e5c 100644 --- a/examples/solid/with-tanstack-router/src/utils/userColumns.tsx +++ b/examples/solid/with-tanstack-router/src/utils/userColumns.tsx @@ -1,23 +1,7 @@ -import type { - CellData, - ColumnDef, - RowData, - TableFeatures, -} from '@tanstack/solid-table' +import type { ColumnDef } from '@tanstack/solid-table' import type { User } from '../api/user' import type { features } from '../components/table' -declare module '@tanstack/solid-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterKey?: keyof TData - filterVariant?: 'text' | 'number' - } -} - export const USER_COLUMNS: Array> = [ { accessorKey: 'id', diff --git a/examples/svelte/kitchen-sink/src/App.svelte b/examples/svelte/kitchen-sink/src/App.svelte index 72ec30550d..a879b0f624 100644 --- a/examples/svelte/kitchen-sink/src/App.svelte +++ b/examples/svelte/kitchen-sink/src/App.svelte @@ -13,8 +13,10 @@ createTableHook, filterFns, FlexRender, + metaHelper, sortFns, stockFeatures, + tableFeatures, } from '@tanstack/svelte-table' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' import { makeData } from './makeData' @@ -30,7 +32,16 @@ } from '@tanstack/svelte-table' import './index.css' - const fuzzyFilter: FilterFn = ( + interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' + } + + const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), + }) + + const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -41,7 +52,7 @@ return itemRank.passed } - const fuzzySort: SortFn = ( + const fuzzySort: SortFn = ( rowA, rowB, columnId, @@ -56,7 +67,7 @@ return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } - const sortStatusFn: SortFn = (rowA, rowB) => { + const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -65,7 +76,7 @@ } const { createAppTable, createAppColumnHelper } = createTableHook({ - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ @@ -78,7 +89,7 @@ groupedRowModel: createGroupedRowModel(aggregationFns), paginatedRowModel: createPaginatedRowModel(), sortedRowModel: createSortedRowModel(sortFns), - } as any, + }, }) const columnHelper = createAppColumnHelper() @@ -187,7 +198,7 @@ ) } - function getCommonPinningStyle(column: Column) { + function getCommonPinningStyle(column: Column) { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = isPinned === 'left' && column.getIsLastColumn('left') @@ -210,15 +221,15 @@ .join('; ') } - function headerStyle(header: Header) { + function headerStyle(header: Header) { return `${getCommonPinningStyle(header.column)}; white-space: nowrap; width: calc(var(--header-${header.id}-size) * 1px)` } - function cellStyle(cell: Cell) { + function cellStyle(cell: Cell) { return `${getCommonPinningStyle(cell.column)}; width: calc(var(--col-${cell.column.id}-size) * 1px)` } - function cellClass(cell: Cell) { + function cellClass(cell: Cell) { const groupingActive = table.state.grouping.length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn return !groupingActive @@ -232,7 +243,7 @@ : undefined } - function pinnedRowStyle(row: Row) { + function pinnedRowStyle(row: Row) { const bottomRows = table.getBottomRows() return [ 'position: sticky', @@ -257,7 +268,7 @@ return styles.join('; ') } - function sortedUniqueValues(column: Column) { + function sortedUniqueValues(column: Column) { if (column.columnDef.meta?.filterVariant === 'range') return [] return Array.from(column.getFacetedUniqueValues().keys()) .sort() @@ -265,7 +276,7 @@ } function updateRangeFilter( - column: Column, + column: Column, index: 0 | 1, value: string, ) { diff --git a/examples/svelte/kitchen-sink/src/table-augmentations.ts b/examples/svelte/kitchen-sink/src/table-augmentations.ts index b089e8d897..0239aa1b4f 100644 --- a/examples/svelte/kitchen-sink/src/table-augmentations.ts +++ b/examples/svelte/kitchen-sink/src/table-augmentations.ts @@ -1,21 +1,7 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' -import type { - CellData, - FilterFn, - RowData, - TableFeatures, - stockFeatures, -} from '@tanstack/svelte-table' +import type { FilterFn, RowData, stockFeatures } from '@tanstack/svelte-table' declare module '@tanstack/svelte-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } - interface FilterFns { fuzzy: FilterFn } diff --git a/examples/vue/kitchen-sink/src/App.vue b/examples/vue/kitchen-sink/src/App.vue index 9d2e41342e..196450f394 100644 --- a/examples/vue/kitchen-sink/src/App.vue +++ b/examples/vue/kitchen-sink/src/App.vue @@ -15,8 +15,10 @@ import { createPaginatedRowModel, createSortedRowModel, filterFns, + metaHelper, sortFns, stockFeatures, + tableFeatures, useTable, } from '@tanstack/vue-table' import { compareItems, rankItem } from '@tanstack/match-sorter-utils' @@ -26,33 +28,33 @@ import type { RankingInfo } from '@tanstack/match-sorter-utils' import type { Person } from './makeData' import type { Cell, - CellData, Column, FilterFn, Header, Row, RowData, SortFn, - TableFeatures, } from '@tanstack/vue-table' declare module '@tanstack/vue-table' { - interface ColumnMeta< - TFeatures extends TableFeatures, - TData extends RowData, - TValue extends CellData = CellData, - > { - filterVariant?: 'text' | 'range' | 'select' - } interface FilterFns { - fuzzy: FilterFn + fuzzy: FilterFn } interface FilterMeta { itemRank?: RankingInfo } } -const fuzzyFilter: FilterFn = ( +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + ...stockFeatures, + columnMeta: metaHelper(), +}) + +const fuzzyFilter: FilterFn = ( row, columnId, value, @@ -63,11 +65,7 @@ const fuzzyFilter: FilterFn = ( return itemRank.passed } -const fuzzySort: SortFn = ( - rowA, - rowB, - columnId, -) => { +const fuzzySort: SortFn = (rowA, rowB, columnId) => { let dir = 0 if (rowA.columnFiltersMeta[columnId]) { dir = compareItems( @@ -78,7 +76,7 @@ const fuzzySort: SortFn = ( return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir } -const sortStatusFn: SortFn = (rowA, rowB) => { +const sortStatusFn: SortFn = (rowA, rowB) => { const statusOrder = ['single', 'complicated', 'relationship'] return ( statusOrder.indexOf(rowA.original.status) - @@ -86,7 +84,7 @@ const sortStatusFn: SortFn = (rowA, rowB) => { ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const columns = ref( columnHelper.columns([ @@ -163,7 +161,7 @@ const data = ref(makeData(1_000)) const table = useTable({ key: 'kitchen-sink', // needed for devtools - features: stockFeatures, + features, rowModels: { expandedRowModel: createExpandedRowModel(), filteredRowModel: createFilteredRowModel({ @@ -221,7 +219,7 @@ function debounceSet(key: string, setValue: () => void) { } function getCommonPinningStyles( - column: Column, + column: Column, ): CSSProperties { const isPinned = column.getIsPinned() const isLastLeftPinnedColumn = @@ -244,7 +242,7 @@ function getCommonPinningStyles( } function headerStyle( - header: Header, + header: Header, ): CSSProperties { return { ...getCommonPinningStyles(header.column), @@ -253,14 +251,14 @@ function headerStyle( } } -function cellStyle(cell: Cell) { +function cellStyle(cell: Cell) { return { ...getCommonPinningStyles(cell.column), width: `calc(var(--col-${cell.column.id}-size) * 1px)`, } } -function cellClass(cell: Cell) { +function cellClass(cell: Cell) { const groupingActive = table.atoms.grouping.get().length > 0 const hasAggregation = !!cell.column.columnDef.aggregationFn return !groupingActive @@ -274,7 +272,7 @@ function cellClass(cell: Cell) { : undefined } -function rowStyle(row: Row): CSSProperties { +function rowStyle(row: Row): CSSProperties { const bottomRows = table.getBottomRows() return { position: 'sticky', @@ -290,7 +288,7 @@ function rowStyle(row: Row): CSSProperties { } } -function sortedUniqueValues(column: Column) { +function sortedUniqueValues(column: Column) { if (column.columnDef.meta?.filterVariant === 'range') return [] return Array.from(column.getFacetedUniqueValues().keys()) .sort() @@ -298,7 +296,7 @@ function sortedUniqueValues(column: Column) { } function updateRangeFilter( - column: Column, + column: Column, index: 0 | 1, value: string, ) { diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index e32365f35f..b1a55726c8 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -35,9 +35,16 @@ export function constructTable< >(tableOptions: TableOptions): Table { const _reactivity = tableOptions.features.coreReactivityFeature! + // `tableMeta`/`columnMeta` are type-only slots, not real features + const { + columnMeta: _columnMeta, + tableMeta: _tableMeta, + ...features + } = tableOptions.features + const table = { _reactivity, - _features: { ...coreFeatures, ...tableOptions.features }, + _features: { ...coreFeatures, ...features }, _rowModels: {}, _rowModelFns: {}, baseAtoms: {}, diff --git a/packages/table-core/src/core/table/coreTablesFeature.types.ts b/packages/table-core/src/core/table/coreTablesFeature.types.ts index cc2f9b5245..c39a199b12 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.types.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.types.ts @@ -3,7 +3,7 @@ import type { Atom, ReadonlyAtom, ReadonlyStore } from '@tanstack/store' import type { CoreFeatures } from '../coreFeatures' import type { RowModelFns } from '../../types/RowModelFns' import type { RowData, Updater } from '../../types/type-utils' -import type { TableFeatures } from '../../types/TableFeatures' +import type { IsAny, TableFeatures } from '../../types/TableFeatures' import type { CachedRowModels, CreateRowModels_All } from '../../types/RowModel' import type { TableOptions } from '../../types/TableOptions' import type { TableState, TableState_All } from '../../types/TableState' @@ -13,6 +13,24 @@ export interface TableMeta< TData extends RowData, > {} +/** + * Resolves the type of `options.meta` for a feature set. + * + * When the features object declares a `tableMeta` type-only slot + * (`tableFeatures({ ..., tableMeta: {} as MyTableMeta })`), that type wins. + * Otherwise this falls back to the global declaration-merged `TableMeta` + * interface. + */ +export type ExtractTableMeta< + TFeatures extends TableFeatures, + TData extends RowData, +> = + IsAny extends true + ? TableMeta + : TFeatures extends { tableMeta: infer TMeta extends object } + ? TMeta + : TableMeta + /** * A map of writable atoms, one per `TableState` slice. These are the internal * writable atoms that the library always writes to via `makeStateUpdater`. @@ -116,8 +134,11 @@ export interface TableOptions_Table< ) => TableOptions /** * You can pass any object to `options.meta` and access it anywhere the `table` is available via `table.options.meta`. + * + * Declare its type per-table via the `tableMeta` type-only slot on the + * `features` option, or globally via declaration merging on `TableMeta`. */ - readonly meta?: TableMeta + readonly meta?: ExtractTableMeta /** * Optionally provide externally managed values for individual state slices. * diff --git a/packages/table-core/src/helpers/metaHelper.ts b/packages/table-core/src/helpers/metaHelper.ts new file mode 100644 index 0000000000..eae1676100 --- /dev/null +++ b/packages/table-core/src/helpers/metaHelper.ts @@ -0,0 +1,24 @@ +/** + * A helper for declaring the `tableMeta`/`columnMeta` type-only slots in the + * `features` option without a type assertion. + * + * Equivalent to `{} as TMeta`, but reads as type-only at the call site and + * avoids `@typescript-eslint/no-unnecessary-type-assertion` false positives + * when the meta type has only optional properties (where an auto-fix removing + * the assertion would silently degrade the inferred meta type to `{}`). + * + * The returned value is a phantom — it is ignored and stripped from the + * table's registered features at runtime; only its type is used. + * @example + * ``` + * import { metaHelper, tableFeatures, rowSortingFeature } from '@tanstack/react-table' + * const features = tableFeatures({ + * rowSortingFeature, + * tableMeta: metaHelper(), + * columnMeta: metaHelper(), + * }); + * ``` + */ +export function metaHelper(): TMeta { + return {} as TMeta +} diff --git a/packages/table-core/src/helpers/tableFeatures.ts b/packages/table-core/src/helpers/tableFeatures.ts index 046c35fb15..c031ce896c 100644 --- a/packages/table-core/src/helpers/tableFeatures.ts +++ b/packages/table-core/src/helpers/tableFeatures.ts @@ -4,10 +4,20 @@ import type { TableFeatures } from '../types/TableFeatures' * A helper function to help define the features that are to be imported and applied to a table instance. * Use this utility to make it easier to have the correct type inference for the features that are being imported. * **Note:** It is recommended to use this utility statically outside of a component. + * + * You can also declare per-table `meta` types here with the `tableMeta` and + * `columnMeta` type-only slots instead of using global declaration merging. + * The values are phantom (ignored and stripped at runtime) — only their types + * are used. * @example * ``` * import { tableFeatures, columnVisibilityFeature, rowPinningFeature } from '@tanstack/react-table' - * const features = tableFeatures({ columnVisibilityFeature, rowPinningFeature }); + * const features = tableFeatures({ + * columnVisibilityFeature, + * rowPinningFeature, + * tableMeta: {} as { updateData: (rowIndex: number, columnId: string, value: unknown) => void }, + * columnMeta: {} as { align?: 'left' | 'right' }, + * }); * const table = useTable({ features, rowModels: {}, columns, data }); * ``` */ diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts index 06805fe7ff..5359f86812 100755 --- a/packages/table-core/src/index.ts +++ b/packages/table-core/src/index.ts @@ -22,6 +22,7 @@ export * from './types/type-utils' export * from './core/coreFeatures' export * from './helpers/columnHelper' +export * from './helpers/metaHelper' export * from './helpers/tableFeatures' export * from './helpers/tableOptions' export * from './utils' diff --git a/packages/table-core/src/types/ColumnDef.ts b/packages/table-core/src/types/ColumnDef.ts index 9877741ad4..8e7999c57f 100644 --- a/packages/table-core/src/types/ColumnDef.ts +++ b/packages/table-core/src/types/ColumnDef.ts @@ -1,5 +1,9 @@ import type { CellData, RowData, UnionToIntersection } from './type-utils' -import type { ExtractFeatureMapTypes, TableFeatures } from './TableFeatures' +import type { + ExtractFeatureMapTypes, + IsAny, + TableFeatures, +} from './TableFeatures' import type { CellContext } from '../core/cells/coreCellsFeature.types' import type { HeaderContext } from '../core/headers/coreHeadersFeature.types' import type { ColumnDef_ColumnFiltering } from '../features/column-filtering/columnFilteringFeature.types' @@ -17,6 +21,25 @@ export interface ColumnMeta< TValue extends CellData = CellData, > {} +/** + * Resolves the type of `columnDef.meta` for a feature set. + * + * When the features object declares a `columnMeta` type-only slot + * (`tableFeatures({ ..., columnMeta: {} as MyColumnMeta })`), that type wins. + * Otherwise this falls back to the global declaration-merged `ColumnMeta` + * interface. + */ +export type ExtractColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +> = + IsAny extends true + ? ColumnMeta + : TFeatures extends { columnMeta: infer TMeta extends object } + ? TMeta + : ColumnMeta + /** * Reads a cell value from an original row object. * @@ -96,8 +119,11 @@ interface ColumnDefBase_Core< cell?: ColumnDefTemplate> /** * User-defined metadata available on the resolved column definition. + * + * Declare its type per-table via the `columnMeta` type-only slot on the + * `features` option, or globally via declaration merging on `ColumnMeta`. */ - meta?: ColumnMeta + meta?: ExtractColumnMeta } export interface ColumnDef_FeatureMap< diff --git a/packages/table-core/src/types/TableFeatures.ts b/packages/table-core/src/types/TableFeatures.ts index dbc8cd0272..4758b1bddb 100644 --- a/packages/table-core/src/types/TableFeatures.ts +++ b/packages/table-core/src/types/TableFeatures.ts @@ -7,7 +7,7 @@ import type { TableOptions_All } from './TableOptions' import type { TableState_All } from './TableState' import type { StockFeatures } from '../features/stockFeatures' -type IsAny = 0 extends 1 & T ? true : false +export type IsAny = 0 extends 1 & T ? true : false type UnionToIntersectionOrEmpty = [T] extends [never] ? {} : UnionToIntersection & {} @@ -25,7 +25,29 @@ export type ExtractFeatureMapTypes< export interface Plugins {} export interface TableFeatures - extends Partial, Partial, Partial {} + extends Partial, Partial, Partial { + /** + * Type-only slot for declaring the type of this table's `options.meta`. + * + * Pass a phantom value: `tableMeta: {} as MyTableMeta`. The value itself is + * ignored and stripped from the table's registered features at runtime — only + * its type is used, inferred wherever `TFeatures` flows. + * + * When omitted, the global declaration-merged `TableMeta` interface applies. + */ + tableMeta?: object + /** + * Type-only slot for declaring the type of `columnDef.meta` for all columns + * of this table. + * + * Pass a phantom value: `columnMeta: {} as MyColumnMeta`. The value itself is + * ignored and stripped from the table's registered features at runtime — only + * its type is used, inferred wherever `TFeatures` flows. + * + * When omitted, the global declaration-merged `ColumnMeta` interface applies. + */ + columnMeta?: object +} export interface TableFeature { /** diff --git a/packages/table-core/src/types/TableOptions.ts b/packages/table-core/src/types/TableOptions.ts index 9a9b48a1bd..b57a46e4f2 100644 --- a/packages/table-core/src/types/TableOptions.ts +++ b/packages/table-core/src/types/TableOptions.ts @@ -34,7 +34,10 @@ export interface TableOptions_Core< TableOptions_Rows {} type DebugKeysFor = { - [K in keyof TFeatures & string as `debug${Capitalize}`]?: boolean + [K in Exclude< + keyof TFeatures & string, + 'tableMeta' | 'columnMeta' // type-only slots, not real features + > as `debug${Capitalize}`]?: boolean } export type DebugOptions = { diff --git a/packages/table-core/tests/unit/core/table/metaTypeSlots.test.ts b/packages/table-core/tests/unit/core/table/metaTypeSlots.test.ts new file mode 100644 index 0000000000..0523370432 --- /dev/null +++ b/packages/table-core/tests/unit/core/table/metaTypeSlots.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest' +import { + constructTable, + coreFeatures, + metaHelper, + rowSortingFeature, + tableFeatures, +} from '../../../../src' +import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' +import type { + CellData, + ColumnMeta, + ExtractColumnMeta, + ExtractTableMeta, + TableMeta, + TableOptions, +} from '../../../../src' + +type Equal = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false +type Expect = T + +interface Person { + firstName: string + age: number +} + +interface MyTableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void +} + +interface MyColumnMeta { + align?: 'left' | 'right' +} + +describe('tableMeta/columnMeta type-only feature slots', () => { + const features = tableFeatures({ + rowSortingFeature, + tableMeta: {} as MyTableMeta, + // `metaHelper` is equivalent to `{} as MyColumnMeta`, but avoids + // no-unnecessary-type-assertion lint errors on all-optional meta types + columnMeta: metaHelper(), + }) + + it('strips the type-only slots from the registered features at runtime', () => { + const table = constructTable({ + features: { + ...coreFeatures, + coreReactivityFeature: storeReactivityBindings(), + ...features, + }, + columns: [], + data: [] as Array, + meta: { + updateData: () => {}, + }, + }) + + expect(table._features).not.toHaveProperty('tableMeta') + expect(table._features).not.toHaveProperty('columnMeta') + expect(table._features).toHaveProperty('rowSortingFeature') + expect(table.options.meta?.updateData).toBeTypeOf('function') + }) + + it('infers meta types from the features object', () => { + type _extractedTableMeta = Expect< + Equal, MyTableMeta> + > + type _extractedColumnMeta = Expect< + Equal, MyColumnMeta> + > + + // options.meta resolves to the declared table meta type + type _optionsMeta = Expect< + Equal< + TableOptions['meta'], + MyTableMeta | undefined + > + > + + // feature option extraction is undisturbed by the phantom keys + type _featureOptions = Expect< + 'onSortingChange' extends keyof TableOptions + ? true + : false + > + + // no debug options are generated for the type-only slots + type _noDebugKeys = Expect< + Equal< + Extract< + keyof TableOptions, + 'debugTableMeta' | 'debugColumnMeta' + >, + never + > + > + + expect(true).toBe(true) + }) + + it('falls back to declaration-merged interfaces when slots are omitted', () => { + const plainFeatures = tableFeatures({ rowSortingFeature }) + + type _tableMetaFallback = Expect< + Equal< + ExtractTableMeta, + TableMeta + > + > + type _columnMetaFallback = Expect< + Equal< + ExtractColumnMeta, + ColumnMeta + > + > + + // internal `any` feature paths also use the fallback + type _anyFallback = Expect< + Equal, TableMeta> + > + + expect(true).toBe(true) + }) +})