Tree-shakeable feature typing dilemma in v9 (react) #6535
Replies: 5 comments
|
The reason none of your patterns work is that this isn't a matter of finding the right constraint. Reproduced on function A<TFeatures extends TableFeatures, TData extends RowData>({
table,
}: {
table: Table<TFeatures, TData>
}) {
return table.store.state.sorting
// ^ Property 'sorting' does not exist on type
// '{} | (TableState_CellSelection & … & TableState_RowSorting)'
}Look at the type in that error: a union of That is also why tightening the bound changes nothing: TFeatures extends TableFeatures & Required<Pick<TableFeatures, 'rowSortingFeature'>>
// identical errorThe union comes from the deferred conditional, not from the constraint, so no bound on There is a second thing worth knowing: That third parameter is the way out. import type {
ReactTable,
RowData,
TableFeatures,
TableState_RowSorting,
} from '@tanstack/react-table'
export function SortingAware<
TFeatures extends TableFeatures,
TData extends RowData,
TState extends TableState_RowSorting,
>({ table }: { table: ReactTable<TFeatures, TData, TState> }) {
return table.state.sorting // resolved — no assertion
}Checked against tables with different feature sets: const small = tableFeatures({ rowSortingFeature, columnVisibilityFeature })
const big = tableFeatures({ rowSortingFeature, columnVisibilityFeature, rowSelectionFeature })
SortingAware({ table: useTable({ features: small, columns, data }) }) // ok
SortingAware({ table: useTable({ features: big, columns, data }) }) // ok
const noSorting = tableFeatures({ columnVisibilityFeature })
SortingAware({ table: useTable({ features: noSorting, columns, data }) })
// Type 'ReactTable<…, TableState_ColumnVisibility>' is not assignable to
// 'ReactTable<…, TableState_RowSorting>'Any feature set containing sorting is accepted, the ones without it are rejected at the call site, and nothing is asserted — which is what you were after. For the components that take a export function ColumnSortingAware<TFeatures extends TableFeatures, TData extends RowData>({
column,
}: {
column: Column<TFeatures, TData, unknown> & Column_RowSorting<TFeatures, TData>
}) {
return column.getIsSorted()
}
// call site — accepted:
// table.getAllColumns().map((column) => <ColumnSortingAware column={column} />)
|
|
What about subtyping column definitions, for example? A |
|
I'm having some challenges migrating an app to v9 too. It's currently running on v8 and using ShadCn components. Because those components are intended to be composed together, there's a deep level of nesting and in order to avoid props drilling from parent to child (and up to grandchildren several levels deep), React's |
|
I also had quite some trouble with this typing and I found a bandaid solution and some better approaches for now, I'll share it below in case it helps anyone. GoalTo create a generic component that accepts Problem 1Using concrete type for TFeatures breaks component consumers. interface TableFeatures extends Partial<CoreFeatures>, Partial<StockFeatures>, Partial<Plugins> {...}type TableFeaturesWithRowPagination = TableFeatures &
Required<Pick<TableFeatures, "rowPaginationFeature">>;
export function DatatablePagination<TData extends RowData>({
table,
}: {
table: ReactTable<TableFeaturesWithRowPagination, TData>; // <- concrete type
}) {
return (
/* state is correctly resolved and can select the pagination state */
<table.Subscribe selector={(state) => state.pagination}>
{(pagination) => (
<Pagination
value={pagination.pageIndex + 1}
total={table.getPageCount()}
onChange={(page) => table.setPageIndex(page - 1)} {/* <- table apis are discovered and no issues in typescript */}
/>
)}
</table.Subscribe>
);
}
// in other components
<DatatablePagination table={table} /> {/* <- typescript error */}This is the typescript error from
Problem 2Using generic type for TFeatures breaks table object in component definition, the table apis are not discoverable. export function DatatablePagination<
TData extends RowData,
TFeatures extends TableFeaturesWithRowPagination,
>({
table,
}: {
table: ReactTable<TFeatures, TData>; // <generic type
}) {
return (
/* state is now typed as ExtractFeatureMapTypes<TFeatures, TableState_FeatureMap>, type cannot be resolved
TS2339: Property pagination does not exist on type {} | (TableState_RowPagination & TableState_CellSelection & TableState_ColumnFiltering & ... 10 more ... & TableState_RowSorting) Property pagination does not exist on type {} */
<table.Subscribe selector={(state) => state.pagination}>
{(pagination) => (
<Pagination
value={pagination.pageIndex + 1}
total={table.getPageCount()} /* <- TS2339: Property getPageCount does not exist on type ReactTable<TFeatures, TData */
onChange={(page) => table.setPageIndex(page - 1)} /* <- TS2339: Property setPageIndex does not exist on type ReactTable<TFeatures, TData> */
/>
)}
</table.Subscribe>
);
}
// in other components
<DatatablePagination table={table} /> {/* <- now works fine */}Bandaid SolutionUse generic type to properly resolve table features, then convert it for internal usage to concrete type. This allows the table object to be passed to the component and it throw errors if RowPagination feature is missing, but also allows for the table apis to be discovered inside the component itself. I am using the same base typescript type TableFeaturesWithRowPagination, so should be ok from type safety pov. type TableFeaturesWithRowPagination = TableFeatures &
Required<Pick<TableFeatures, "rowPaginationFeature">>;
// 1. PUBLIC BOUNDARY: Generic constraint for the parent table.
// WHY: Using a concrete type in props forces TanStack's ExtractFeatureMapTypes to
// extract all library features, breaking parent assignment. Generics capture exact features.
export function DatatablePagination<
TData extends RowData,
TFeatures extends TableFeaturesWithRowPagination,
>(props: { table: ReactTable<TFeatures, TData> }) {
// 2. INTERNAL BOUNDARY: Cast the generic back to a concrete type.
// WHY: TypeScript defers mapping on open generics, causing table APIs and state to
// remain unresolved (returning `{}`). Casting forces evaluation to make APIs discoverable.
const table = props.table as unknown as ReactTable<TableFeaturesWithRowPagination, TData>;
return (
<table.Subscribe selector={(state) => state.pagination}>
{(pagination) => (
<Pagination
value={pagination.pageIndex + 1}
total={table.getPageCount()}
onChange={(page) => table.setPageIndex(page - 1)}
/>
)}
</table.Subscribe>
);
}What I went for insteadI started from I was afraid of silent failures of components not re-rendering in cases where I may add few states from other features later and forget to add it in all table subcomponents or use a different My current solution is this:
export function DatatablePagination() {
const table = useTableContext();
return (
<Pagination
value={table.state.pagination.pageIndex + 1}
total={table.getPageCount()}
onChange={(page) => table.setPageIndex(page - 1)}
/>
);
} |
|
Putting my update here too just in case someone else run into this thread. I decided to use |

Uh oh!
There was an error while loading. Please reload this page.
I'm currently struggling a bit with the migration from v8 to v9 in a React project. This is related to the newly introduced dependency of the whole typing system on the tableFeatures that have been selected when creating the table instance with useTable.
Background:
We use quite a few reusable components that take e.g. table or column objects as input, interact with the table, column, etc. API(s) and render stuff as a result of these API calls.
There might be a sorting-aware component that uses information about the sorting state of column(s) and a selection-aware component that accesses information about selected rows. These components can then be used for different all kinds of tables that use different sets of features. In the past, typing was easy, because all features were imported by default, so all APIs were always present on each table, column, ... instance.
Problem in the migration process:
When typing e.g. a table input prop of such a component as { table: Table<TFeatures extends TableFeatures, TData extends RowData>}, by default I will not be able to use most of the table API methods, because TableFeatures is pretty bare-bones. That means, I somehow need to narrow the type to make sure typescript understands that the e.g. sorting API methods are available in the component code. At the same time, I don't want to type the table too specifically for a certain set of features because I want to use the component for a table that has e.g. the feature sets {rowSortingFeature, columnVisibilityFeature}, {rowSortingFeature, rowSelectionFeature, columnVisibilityFeature} and all kinds of conceivable feature combinations.
I've tried some typing patterns in the following codesandbox (see file src/SortingAware.tsx): https://codesandbox.io/p/devbox/polished-hazelnut-vktpys?file=%2Fsrc%2FSortingAware.tsx
None of them really works, and my only current workarounds are really nasty type assertions (which sometimes work, sometimes not but can get easily out of hands for complex components). Am I overlooking something really obvious here? What is a clean pattern for achieving safe and consistent typing?
All reactions