From 572c298f6097ac26c9208b39a74e0af16ea0ba87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Wed, 23 Sep 2026 13:46:16 +0200 Subject: [PATCH] Keep the Cell of a column checked, and let any column be read #202 makes `CellArgs` default to `any` on `Column` and `Table`, so that a column whose Cell asks for args fits a `Column` parameter, which the plugin helpers need. It also stops checking the Cell of every column written by hand. `Column['Cell']` is `ComponentLike`: it takes any args and requires none, not even `@row` and `@column`. On 4.0.0 it was `ComponentLike>`. 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 --- .../-private/-type-tests/cell-args.test.ts | 7 +++- table/src/-private/cell-component.ts | 32 +++++++++++++++++++ table/src/-private/column.ts | 7 ++-- table/src/-private/table.ts | 4 +-- test-app/tests/integration/cells-test.gts | 22 ++++++++++++- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 table/src/-private/cell-component.ts diff --git a/table/src/-private/-type-tests/cell-args.test.ts b/table/src/-private/-type-tests/cell-args.test.ts index 31e9c5c..068b8ba 100644 --- a/table/src/-private/-type-tests/cell-args.test.ts +++ b/table/src/-private/-type-tests/cell-args.test.ts @@ -15,6 +15,7 @@ import { meta } from '../../plugins/index.ts'; import type { CellContext, Column, ColumnConfig, Table } from '../../index.ts'; import type { ComponentLike } from '@glint/template'; +import type { CellComponent } from '../cell-component.ts'; interface Person { name: string; @@ -37,7 +38,11 @@ declare const UpdateCell: ComponentLike< declare const PlainCell: ComponentLike>; type CellArgsOf = - NonNullable extends ComponentLike ? Args : never; + NonNullable extends CellComponent + ? Args + : NonNullable extends ComponentLike + ? Args + : never; ///////////////////////////////////////////// // The args of the Cells are inferred, besides `@row` and `@column` diff --git a/table/src/-private/cell-component.ts b/table/src/-private/cell-component.ts new file mode 100644 index 0000000..23f1f76 --- /dev/null +++ b/table/src/-private/cell-component.ts @@ -0,0 +1,32 @@ +import type { + ComponentReturn, + FlattenBlockParams, + Invoke, +} from '@glint/template/-private/integration'; +import type { + ComponentSignatureArgs, + ComponentSignatureBlocks, + ComponentSignatureElement, + InvokableArgs, +} from '@glint/template/-private/signature'; + +/** + * A component rendered as a cell, invoked the way `ComponentLike` is. + * + * `[Invoke]` is a method here, where `ComponentLike` declares it as a property. + * TypeScript compares the parameters of a method bivariantly, so a column whose + * Cell asks for args fits a `Column` that names none, while the args + * are still checked where the Cell is rendered. + * + * With a property, those two cannot both hold: the args of a property function + * are contravariant, so the only way to make such a column fit is `any`, which + * stops the Cell being checked at all. + */ +export type CellComponent = abstract new (...args: any) => { + [Invoke]( + ...args: InvokableArgs> + ): ComponentReturn< + FlattenBlockParams>, + ComponentSignatureElement + >; +}; diff --git a/table/src/-private/column.ts b/table/src/-private/column.ts index 92c9884..a247693 100644 --- a/table/src/-private/column.ts +++ b/table/src/-private/column.ts @@ -1,6 +1,7 @@ import { action, get } from '@ember/object'; import { isEmpty } from '@ember/utils'; +import type { CellComponent } from './cell-component.ts'; import type { Row } from './row'; import type { Table } from './table'; import type { ComponentLike, ContentValue } from '@glint/template'; @@ -24,13 +25,13 @@ export class Column< T = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { get Cell(): - | ComponentLike & CellArgs> + | CellComponent & CellArgs> | undefined { return this.config.Cell as - | ComponentLike & CellArgs> + | CellComponent & CellArgs> | undefined; } diff --git a/table/src/-private/table.ts b/table/src/-private/table.ts index 58e90ee..3a0eb05 100644 --- a/table/src/-private/table.ts +++ b/table/src/-private/table.ts @@ -55,7 +55,7 @@ export interface Table< DataType = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { /** * @private @@ -87,7 +87,7 @@ export class Table< DataType = unknown, ColumnMeta = unknown, Meta = unknown, - CellArgs = any, + CellArgs = unknown, > { /** * @private diff --git a/test-app/tests/integration/cells-test.gts b/test-app/tests/integration/cells-test.gts index 332683a..9c0cc09 100644 --- a/test-app/tests/integration/cells-test.gts +++ b/test-app/tests/integration/cells-test.gts @@ -6,7 +6,7 @@ import { setupRenderingTest } from "ember-qunit"; import { headlessTable } from "@universal-ember/table"; import type { TOC } from "@ember/component/template-only"; -import type { CellContext } from "@universal-ember/table"; +import type { CellContext, Column } from "@universal-ember/table"; interface Person { name: string; @@ -133,3 +133,23 @@ const TypeChecks: TOC<{ Args: { table: Context["table"] } }> = ; void TypeChecks; + +/** + * A column written by hand, rather than one a table inferred. + * + * Its Cell takes `@row` and `@column` and nothing else, so leaving them out is + * an error. On 4.0.0 `Cell` was `ComponentLike>` and this held. + */ +declare const handWritten: Column; + +const HandWrittenColumnChecks: TOC = ; + +void HandWrittenColumnChecks;