Skip to content

ObjectGridProps declares 12 props while ObjectGrid reads 12 more through (rest as any) — the external-pagination contract is undeclared #4277

Description

@yinlianghui

Found while wiring @object-ui/plugin-grid's tests into tsc for #4040 (tranche 1). Filed rather than fixed: closing it changes a public exported interface, which is a contract decision, not execution. @object-ui/plugin-grid stays in TEST_DEBT until this is ruled on — its test project cannot go green without either this decision or an as any at the call site, and the second would encode exactly the wrong understanding this whole card exists to stop.

What the compiler says the moment it is allowed to look

packages/plugin-grid/src/__tests__/serverPagination.test.tsx, with the #3032 test-project template applied:

src/__tests__/serverPagination.test.tsx(182,11): error TS2322: Type '{ schema: any; dataSource: any; data: { id: string; name: string; }[]; manualPagination: true; rowCount: number; page: number; pageSize: number; onPageChange: Mock< Procedure >; onPageSizeChange: Mock< ... >; }' is not assignable to type 'IntrinsicAttributes & ObjectGridProps'.
  Property 'data' does not exist on type 'IntrinsicAttributes & ObjectGridProps'.

The gap

ObjectGridProps (src/ObjectGrid.tsx:160) declares 12 members: schema, dataSource, className, onRowClick, onEdit, onDelete, onBulkDelete, onCellChange, onRowSave, onBatchSave, onRowSelect, onAddRecord.

The component collects everything else into ...rest (:269) and reads twelve further props out of it, each through an as any cast:

prop read at
data :384 — const passedData = (rest as any).data;
manualPagination :420
rowCount :421, :2162
onPageChange :422, :2168
page :2163
pageSize :2165
onPageSizeChange :2171
sort :2188
onSortChange :2191
search :2201
onSearchChange :2204
onColumnStateChange :359

These are not accidental. :418 says so in as many words — "data arrives via rest (a prop), so do these too" — and the block implements the host-driven external pagination path from framework #2212. The same vocabulary IS declared one layer down, on DataTableSchema (packages/types/src/data-display.ts:400+), which ObjectGrid builds and forwards to (:2207).

Why nothing has caught it

The only non-test caller that could pass them is ObjectGridRenderer (src/index.tsx:70), typed React.FC< { schema: any; [key: string]: any } > and spreading {...props} straight through. An index signature of any accepts every one of the twelve, so the renderer path compiles and the gap is invisible from there. Every typed caller (plugin-view/src/ObjectView.tsx:1094, plugin-designer ×2, apps/console/src/dev/DevRowActions.tsx) passes only declared props, so none of them feels it either.

And the tests that exercise the path were not compiled by anything, which is #4040's premise exactly.

The decision

Not obviously one-sided, which is why it is filed instead of guessed:

  • A — declare them on ObjectGridProps. Matches what the component really accepts; makes the external-pagination contract checkable at every call site; is a public surface expansion on a widely-consumed block.
  • B — declare them as a separate, documented interface (e.g. ObjectGridExternalPaginationProps) that ObjectGridProps extends or that the host path is typed with. Keeps the "advanced host-driven" mode distinguishable from the ordinary authoring surface.
  • C — rule that the escape hatch is deliberate and the props are internal, in which case the twelve (rest as any) reads want a named internal type and ObjectGridRenderer's any index signature is the thing to tighten.

All three make the test compile without weakening it. Widening nothing and casting in the test is the one option that should not be taken: it would leave the same undeclared contract in place with a green tick over it.

Also seen in the same package, unblocked but not yet fixed

src/__tests__/useBulkExecutor.test.ts:406 — runAggregate is stubbed vi.fn(async () => undefined), no parameters, while BulkExecutorOptions.runAggregate (src/hooks/useBulkExecutor.ts:127) is (def, rows, params) => Promise< unknown > and the hook calls it with all three (:273). The case then asserts on runAggregate.mock.calls[0][1], the second argument — which the declared stub says does not exist:

src/__tests__/useBulkExecutor.test.ts(406,15): error TS2352: Conversion of type 'undefined' to type 'unknown[]' may be a mistake ...
src/__tests__/useBulkExecutor.test.ts(406,42): error TS2493: Tuple type '[]' of length '0' has no element at index '1'.

It passes at runtime — vitest records real arguments whatever the declared signature — so this is a types-only lie, fixable by typing the stub's parameters. Recorded here so it travels with the package rather than being re-discovered.

Also worth knowing for whoever picks this up: TEST_DEBT declares 2 errors for @object-ui/plugin-grid, measured at #3032 branch time. Re-measured against 4cb0562b5 it is 4 code-tier (plus one config-tier TS2550, Array.prototype.at, resolved by the test project's lib). main moved; the fourth is importMissingRequiredHint.test.tsx:35 omitting ImportWizardProps.dataSource, which is a plain fixture fix.

Refs #4040, objectstack-ai/objectstack#4118.

Activity

  1. claude commented on Aug 11, 2026

    @claude
    Contributor

    Findings triage (first grade): promoted to needs-user-decision (finding label swapped out with this comment, per the pair rule).

    Why the box and not the queue: the card is a contract fork on a public exported interface, and it says so itself — ObjectGridProps (verified packages/plugin-grid/src/ObjectGrid.tsx:160 on origin/main @ 90e792e) declares 12 members while the component reads 12 more through (rest as any) (:384 const passedData = (rest as any).data; verified verbatim, with the :418 comment confirming the reads are deliberate — the framework #2212 external-pagination path). Declaring them (public API widening), re-routing through DataTableSchema, or blessing the cast are different contracts; that choice is the maintainer's, not an executor's.

    Why now and not a hold: it has an open in-flight dependent — #4040 tranche 1 keeps @object-ui/plugin-grid in TEST_DEBT until this is ruled, so holding the card blocks dispatched work (this also puts it under the seat's instruction-④ dependency flag in the round brief).

    Dedup: no open objectui card names the ObjectGridProps / external-pagination contract; #4040 references the debt but scopes the ruling out.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    代决(session session_017Qqyix2QcnpUC9XeYVDzx3,objectui whole-repo PM seat;按维护者既有授权以产品长期稳定角度代决,否决窗口开放;分诊注记"该选择属维护者"已阅——本席持有的正是该项转授权,且本卡阻塞在飞的 #4040)。

    裁决:B,加一条派生令。

    1. C 否:12 个 (rest as any) 读实现的是 framework#2212 的宿主驱动外部分页路径,:418 的注释自证 deliberate。把宿主能力裁成"内部逃生门"与实测意图相悖——能力存在就该可声明、可检查。
    2. A 弃,B 立:external-pagination 是与普通 authoring surface 不同类的模式契约,值得独立命名(ObjectGridExternalPaginationProps 或相近),ObjectGridProps extends 之。12 个成员平铺进已有 12 成员的接口会抹掉这条模式边界。
    3. 派生令(反漂移,执行时的硬约束):卡片自己指出同一词汇已在 DataTableSchema(packages/types/src/data-display.ts:400+)声明过一次,且 ObjectGrid 在 :2207 原样转发。新接口中与 DataTableSchema 语义对应的成员必须类型级派生(Pick / 映射)自那份声明,不得手抄第二份 12 项枚举——两处一旦手抄,下次漂移就在这两份之间发生。仅 grid 独有、DataTableSchema 无对应物的成员才允许显式声明,并逐一注明为何无对应。
    4. 实施随裁决落地:12 处 (rest as any) 读改为类型化解构;serverPagination.test.tsx 无 cast 编译;ObjectGridProps declares 12 props while ObjectGrid reads 12 more through (rest as any) — the external-pagination contract is undeclared #4277 记录的另两处机械修复(importMissingRequiredHint fixture、useBulkExecutor stub 签名)同 PR 收尾;plugin-grid 移出 TEST_DEBT。changeset 建议 minor(声明既有接受面 = 能力显形)。ObjectGridRenderer 的 any 索引签名(C 的附带项)不并入——那是所有 block renderer 的通用形状,单独议。

    状态:needs-user-decision → pm:queue + pm:dispatched,并入 #4040 批次2 执行。


    Generated by Claude Code

  3. self-assigned this
    on Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions