Infer the types of column meta and table meta - #202
NullVoxPopuli merged 5 commits into
Conversation
headlessTable infers the type of each column's `meta` and of the table's `meta` from the config, with no type arguments and no helper. - `column.meta` is the merged type of all column metas. A list with a declared type (`ColumnConfig<Person, Meta>[]`) keeps that type. - `table.config.meta` is `TableMeta` plus the inferred meta. - A `Cell` can ask for the column meta and table meta it reads, through `CellContext<T, ColumnMeta, Meta>`. Extra data for cells goes through the table meta. - `columns.for`, `next`, `previous`, `before`, `after` and `orderedColumnsFor` keep the meta types. `value` and `options` see the table meta, but not the column meta: a typed column meta there makes TypeScript fix the metas before it infers them. The mapped column list lives only in `HeadlessTableConfig`, because a mapped type in `TableConfig` makes every `Table` comparison structural, and `Table<X>` would stop fitting `Table<unknown>`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Callbacks were the only place ColumnConfig used T. For a list typed ColumnConfig[], the row type that headlessTable inferred then depended on the order TypeScript checked the program in: TS 5.6 to 6.0 inferred the data type in test-app, and Table<Row> did not fit Table<unknown>. A type-only property that mentions T makes it unknown every time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both comments named causes that did not hold up when checked again: typed callbacks would get `any` from the plain column list, and a mapped type in TableConfig does not break Table comparisons. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@NullVoxPopuli yes, this approach also seems to work well! Column meta and table meta are inferred from annotated column lists, and compared to #200, the One gap on cell args though. Some of our tables give a Cell args from the template, in addition to This type test fails on #202: the column config does not accept the Cell. type GroupedCellArgs = CellContext<Person> & {
groupBy: 'day' | 'week';
onUpdate: (value: string) => void;
};
declare const GroupedCell: ComponentLike<GroupedCellArgs>;
const table = headlessTable({}, {
columns: () => [{ key: 'name', Cell: GroupedCell }],
data: () => people,
});
expectTypeOf(table.columns[0]!.Cell).toEqualTypeOf<
ComponentLike<GroupedCellArgs> | undefined
>();Maybe, a |
A Cell can take args besides @Row and @column, passed where it is rendered (<column.Cell @Row @column @groupby={{...}} />). headlessTable reads them from the Cells of the column list, and column.Cell asks for all of them. Cells whose args differ add up. A column list typed as ColumnConfig<Row, ColumnMeta, TableMeta, CellArgs>[] replaces the inferred args. The args are read the way Glint reads them, so template-only components, class components and ComponentLike all work. A new docs page, "Typing meta and cells", covers what is inferred, how to check it, how to declare it, how to write Cells, and the limits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
cfbb714 closes the cell args gap, thanks🎉 I then tried to migrate our app to this head, and met two blockers: 1. The plugin helpers reject a column whose Cell takes args. declare const GroupedCell: ComponentLike<CellContext<Person> & { groupBy: 'day' | 'week' }>;
const table = headlessTable({}, {
columns: () => [{ key: 'name', Cell: GroupedCell }],
data: () => people,
});
isVisible(table.columns[0]!); // error TS2345
sort(table.columns[0]!); // error TS2345
2. See johanrd#5 for a possible fix. |
|
also johanrd#6 |
…@options - Column and Table default their cell args to `any`, so `Column<Person>` and `Table<Person>` accept a column or table whose Cells take extra args. The plugin helpers (`sort`, `isVisible`, `hide`, ...) take them with no change to their signatures. Tables from headlessTable still carry their inferred args. - `row.table` has the types of the table, like `column.table`, in the rows of a table and in `CellContext`. - `getOptionsForRow` has the `@options` that the Cells ask for, as in johanrd#5. - Tests: each column decides which Cell fits it through its own meta, the plugin helpers accept a column with cell args, and a Cell renders with `@options={{column.getOptionsForRow row}}`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| key: 'name', | ||
| meta: { align: 'left' }, | ||
| // @ts-expect-error this column's meta has no `width` | ||
| Cell: WidthCell, |
universal-ember#202 makes `CellArgs` default to `any` on `Column` and `Table`, so that a column whose Cell asks for args fits a `Column<DataType>` parameter, which the plugin helpers need. It also stops checking the Cell of every column written by hand. `Column<Person>['Cell']` is `ComponentLike<any>`: it takes any args and requires none, not even `@row` and `@column`. On 4.0.0 it was `ComponentLike<CellContext<T>>`. Both hold if the Cell is read through a type whose `[Invoke]` is a method. TypeScript compares the parameters of a method bivariantly, so such a column fits a parameter that names no args, while the args are still checked where the Cell is rendered. The config keeps `ComponentLike`, so a Cell is still checked against its column's meta. The default goes back to `unknown`. The plugin helpers are untouched. Cowritten by Claude
An alternative to #200 that uses inference: no
typesvalue, notableTypes()helper, and no type arguments. A declared type can replace each inferred type.What each need from #200 becomes:
columnMetaslotmeta, then merged.ColumnConfig<Person, Meta>[]declares it instead.satisfieschecks one column in place.tableMetaslotmeta, added toTableMeta.cellArgsslot@rowand@columnthat any Cell takes.ColumnConfig<Person, ColumnMeta, TableMeta, CellArgs>[]declares it instead. Works with template-only components, class components, andComponentLike.CellContext<T, ColumnMeta, TableMeta>. Each Cell is checked against the meta of its own column. TypeScript reports a column or table whose meta does not match.@row.tableand@column.tableboth have the table's types.@optionscolumn.getOptionsForRowhas the@optionsthat the Cells ask for, as in johanrd#5.columns.for,next,previous,before,after,orderedColumnsFor.Column<Person>,Table<Person>,ColumnConfig<Person>[]accept typed tables and columns, also with cell args. So the plugin helpers take them unchanged. No existing test changed.A new docs page, "Typing meta and cells", covers what is inferred, how to check it, how to declare it, how to write Cells, and the limits. The TypeScript page links to it.
Limits
valueandoptions,column.metaisunknown. Typed with the column meta, it would beany: the callbacks take their type from the plain column list next to the mapped one (see below), where the column meta isany. The table meta is typed there.optionsreturns the@optionsits Cell asks for. The return type ofoptionsis only known after the Cell types are fixed.<template>Cell has no types for its args. This is the same onmain.metaliteral needs annotated parameters.Implementation notes
headlessTableinfers a tuple of the column metas through a homomorphic mapped type with aconsttype parameter.ColumnMetaOfmerges the tuple into one object type.meta. Inferring the args from Glint'sComponentLikedirectly keeps only the Cell with the fewest args.CellArgsOfreads each Cell's named args (from theTemplateOnlyComponentsignature, from Glint's[Invoke], or fromargs) and intersects them.HeadlessTableConfig, the parameter type ofheadlessTable, so thatTableConfigandtable.configstay a plain interface. Next to it is a plainreadonly ColumnConfig<DataType, any, Meta, any>[]. TypeScript does not inferDataTypethrough the mapped list, so without the plain listDataTypewould come fromdataonly.ColumnConfighas a type-only[rowType]?: Tproperty, as in Add per-table types: column meta, table meta, cell args #200. Without it, a list typedColumnConfig[]gives a row type that depends on the order TypeScript checks the program in. TS 5.6 to 6.0 failed on this intest-app.ColumnandTabledefault their cell args toany, so a bareColumn<Person>means a column with any Cell. A narrower default made every plugin helper reject a column whose Cell takes args. Tables fromheadlessTablealways carry their inferred args.row.tableis typed through an intersection on the rows and inCellContext. Adding the type parameters to theRowclass madeTableinvariant in its meta.CellContexthasoutvariance annotations. Without them, TypeScript measuresCellContextas unrelated to its meta parameters, becauseColumn,TableandCellContextrefer to each other in cycles. A Cell that needs table meta would then pass on a table without it.Needs TypeScript 5.4 (
NoInfer). The oldest version in the CI matrix is 5.6.Checked:
table/src/-private/-type-tests/(inferred-meta,cell-args,column-controls-cell) pass with TS 5.6, TS 6, and the TS 7 nightly.test-app/tests/integration/cells-test.gtsrenders Cells with extra args and with@options, and checks the Glint errors for a missing or wrong arg.test-apptype-checks with TS 5.6, 6.0, and the TS 7 nightly. CI runs the full matrix.pnpm lint: pass.test-apptests: 189 of 189 pass.docs-appproduction build: pass.🤖 Generated with Claude Code