Skip to content

Add per-table types: column meta, table meta, cell args - #200

Open
johanrd wants to merge 8 commits into
universal-ember:mainfrom
johanrd:per-table-meta
Open

johanrd wants to merge 8 commits into
universal-ember:mainfrom
johanrd:per-table-meta

Conversation

@johanrd

@johanrd johanrd commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Replaces #111 . A table declares its types once. They apply wherever the table is read: column.meta, table.config.meta and the args of column.Cell, also inside value, options and a Cell.

interface ReportTypes {
  columnMeta: { align?: 'left' | 'right' };
  cellArgs: { dateRange: DateRange };
}

headlessTable(this, { types: tableTypes<ReportTypes>(), columns, data });
// <column.Cell @row={{row}} @column={{column}} @dateRange={{@dateRange}} />

Cowritten by Claude

- ColumnConfig.meta: static facts about a column, typed by the
  augmentable ColumnMeta<T>, read as column.meta without a row.
- column<T>(): checks one column's Cell against its own options.
- Export CellContext, CellOptions, CellArgs, TypedColumnConfig.

Table, Column and the plugins keep their type parameters.
A table declares its own types once:

  interface ReportTypes { columnMeta: ...; tableMeta: ... }
  headlessTable(this, { types: tableTypes<ReportTypes>(), ... })

column.meta and table.config.meta then have these types wherever they
are read, also inside value, options and Cell. The slot names are those
of TanStack Table.

Tables that declare nothing are typed as before. Seven test sites
annotated a bare Table and assigned a typed table to it; that never was
assignable, so they name the table's type now.
@NullVoxPopuli

Copy link
Copy Markdown
Contributor

why not pass a generic to (headless)table? why add values? looks like the tableTypes function does nothing, and is only for passing types anyway?

@johanrd

johanrd commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@NullVoxPopuli I haven't been able to get past TypeScript's "all-or-nothing" rule for type arguments.

headlessTable infers T from data, and the moment you write any type argument, inference seems to stop for all of them?

  • headlessTable<Person, ReportTypes>(…) works, but now every table need to spell out its row type by hand. (it would be ok for me, but it would be a breaking change, and could be a bit picky for others)
  • headlessTable<ReportTypes>(…) is an error — the lone argument lands on T.
  • Putting Types first so it can be named alone makes T fall back to its default, unknown.

tableTypes<ReportTypes>() in this PR is a runtime no-op whose only job is to carry the type into the config, so Types is inferred from the value and T stays inferred from data.

It's the same pattern TanStack v9 uses (columnMeta: metaHelper<…>() inside tableFeatures), which is where the slot names come from. I considered naming it tableFeatures for 1:1 wit tanstack, but that would be too long of a stretch until plugins etc are also meant to be assed into that.

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

thanks! more questions / comments:

headlessTable infers T from data, and the moment you write any type argument, inference seems to stop for all of them?

this is why an explicit type arg can be passed

headlessTable<Person, ReportTypes>(…) works

why are there two type args? I don't understand -- current API is just one

the lone argument lands on T.

what does this mean?

TanStack Table v9

I don't think because someone else does sothing, it means it's the best way. The goal of this library is not to be compatible with tanstack, but to lean in to to the things we can do better, while taking inspiration where we can.

@johanrd

johanrd commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

yes, DataType = T as the typed Row already works, but my motivation is to have access to strong types also of the columns and the metas in the templates.

That is the second type parameter this PR adds: <DataType, TableTypeSlots>, where TableTypeSlots carries columnMeta, tableMeta and cell args into column.meta etc. So the question is only how a table declares TableTypeSlots without losing the inference of DataType it already has.

Type arguments are positional and all-or-nothing. headlessTable<ReportTypes>(…) puts ReportTypes into DataType (that's "lands on T"), and headlessTable<Person, ReportTypes>(…) works but makes every typed table spell out its row type — a breaking change.

I don't think because someone else does sothing, it means it's the best way.

I'm not saying Tanstack v9 is necessarily the best for us, but so far it is the only trick i've found that does not break the inferance of DataType and at the same time provide strong types for column and meta.

What I tried on this branch against `tsc`:
  • infer Types from the column configs → resolves to bare TableTypes; metas stay untyped
  • declare tableTypes: ReportTypes on the component, read from the this we already pass → works, but only as this as Report; inside a field initializer this is the polymorphic this, so it can't resolve at the definition site
  • currying headlessTable<ReportTypes>()(this, …) → works, changes every call
  • a typed value in the config, types: tableTypes<ReportTypes>() → works, nothing existing changes
  • interring from the passed in this
  • Add generic type parameters for optional strong typing of Options and Cell component for template type checking #111

The helper is just a hidden {} as ReportTypes; columns/meta are NoInfer<Types> so that slot is the only source.

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

thanks for the background (and PR) -- I'm going to attempt an experiment with claude, and see if things improve any -- maybe what you have provided here really is the best way (but I also don't see a problem in breaking changes)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants