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.