From b7a22732c437eaf79c8aa0d67b6ab5ee1298e16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Wed, 23 Sep 2026 01:17:00 +0200 Subject: [PATCH] Keep the cell args a declared column list states `ColumnMetaOf` reads the meta a declared list states, because such a list is a plain array with no tuple members to read. `CellArgsOf` has no such branch, so it always goes back to the Cells, and a list whose args are a type parameter has no Cell to read them from. A component that takes a column list from its caller and builds the table itself could therefore not keep the args of that list. The args now come from the config, the way the row type already does. A list written in place still reads its Cells. Cowritten by Claude --- .../-type-tests/generic-wrapper.test.ts | 40 +++++++++++++++++++ table/src/-private/interfaces/column.ts | 12 ++++++ table/src/-private/meta.ts | 22 +++++++--- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 table/src/-private/-type-tests/generic-wrapper.test.ts diff --git a/table/src/-private/-type-tests/generic-wrapper.test.ts b/table/src/-private/-type-tests/generic-wrapper.test.ts new file mode 100644 index 0000000..c8c1255 --- /dev/null +++ b/table/src/-private/-type-tests/generic-wrapper.test.ts @@ -0,0 +1,40 @@ +import { expectTypeOf } from 'expect-type'; + +import { headlessTable } from '../../index.ts'; + +import type { CellContext, ColumnConfig } from '../../index.ts'; +import type { ComponentLike } from '@glint/template'; + +interface Person { + name: string; +} +declare const people: Person[]; + +// A shared table component takes a column list from its caller and builds the +// table itself. Its own cell args are a type parameter, so there is no Cell to +// read them from. +function makeTable( + columnConfigList: ColumnConfig[], + data: T[], +) { + return headlessTable( + {}, + { columns: () => columnConfigList, data: () => data }, + ); +} + +declare const columnList: ColumnConfig< + Person, + unknown, + unknown, + { groupBy: 'day' | 'week' } +>[]; + +const table = makeTable(columnList, people); + +expectTypeOf(table.columns[0]!.Cell).toEqualTypeOf< + | ComponentLike< + CellContext & { groupBy: 'day' | 'week' } + > + | undefined +>(); diff --git a/table/src/-private/interfaces/column.ts b/table/src/-private/interfaces/column.ts index 0fee553..41f35eb 100644 --- a/table/src/-private/interfaces/column.ts +++ b/table/src/-private/interfaces/column.ts @@ -7,6 +7,7 @@ import type { Constructor } from '../private-types'; import type { ComponentLike, ContentValue } from '@glint/template'; declare const rowType: unique symbol; +declare const cellArgsType: unique symbol; /** * What `value`, `options`, and a `Cell` receive. @@ -26,6 +27,8 @@ type ColumnPluginOption

= P extends BasePlugin ? [Constructor

, () => ColumnOptionsFor>] : [P | Constructor

, () => unknown]; +export type { cellArgsType }; + export type CellOptions = { /** * when no value is present for a given set of data for the given column config @@ -113,6 +116,15 @@ export interface ColumnConfig< * gives `headlessTable` a row type that depends on the order TypeScript checks the program in. */ readonly [rowType]?: T; + + /** + * Type-only, never set. + * + * A column list written in place is a tuple, and the args come from its Cells. + * A list with a declared type is a plain array, which has no Cells to read, + * so the args it declares are read from here. + */ + readonly [cellArgsType]?: CellArgs; } export type ColumnKey = NonNullable['key']>; diff --git a/table/src/-private/meta.ts b/table/src/-private/meta.ts index 0c43e16..a24d946 100644 --- a/table/src/-private/meta.ts +++ b/table/src/-private/meta.ts @@ -4,6 +4,7 @@ import type { UnwrapNamedArgs, } from '@glint/template/-private/integration'; import type { ComponentSignatureArgs } from '@glint/template/-private/signature'; +import type { cellArgsType } from './interfaces/column.ts'; /** * The metas of columns that set one. @@ -94,11 +95,22 @@ type UnionToIntersection = ( * [{ Cell: GroupByCell }, { Cell: UpdateCell }] * → { groupBy: ... } & { onUpdate: ... } */ -export type CellArgsOf = [ - ProvidedCells, -] extends [never] - ? unknown - : UnionToIntersection>>; +export type CellArgsOf = + number extends Columns['length'] + ? DeclaredCellArgs + : [ProvidedCells] extends [never] + ? unknown + : UnionToIntersection>>; + +/** + * The cell args a column config declares, for a list that is a plain array. + * Such a list has no Cells to read the args from, the way a tuple does. + */ +type DeclaredCellArgs = Column extends { + [cellArgsType]?: infer CellArgs; +} + ? CellArgs + : unknown; /** * The `@options` the Cells of a table ask for, from their args.