Skip to content

finding(components): planDataTableRowMenu's predicate parameter is narrower than the object its only caller hands it #4354

Description

@yinlianghui

Found while type-checking @object-ui/components' tests for #4040 (tranche 4). Observation-class: no runtime symptom — the value flows through untouched either way.

What

planDataTableRowMenu (packages/components/src/renderers/complex/data-table.tsx) declares:

editPredicates?: { visibleWhen?: unknown };
deletePredicates?: { visibleWhen?: unknown };

Its only production caller passes schema.rowEditPredicates / schema.rowDeletePredicates, whose declared shape in @object-ui/types is:

rowEditPredicates?: { visibleWhen?: unknown; disabledWhen?: unknown };

and the row-menu ITEM component two functions down the same file declares the full pair (predicates?: { visibleWhen?: unknown; disabledWhen?: unknown }) because it evaluates both. So the planner's parameter is a third, narrower restatement of one authoring shape — the #3009 hand-copy family, in miniature.

It is arguably deliberate: the planner decides visibility only, so its parameter names only what it consumes. That reading is defensible; what makes it worth recording is the consequence below.

How it surfaced

data-table-row-menu-empty-guard.test.ts's case still counts an item that renders merely DISABLED passed editPredicates: { disabledWhen: 'record.frozen == true' } as a bare object literal. Excess-property checking rejects that against { visibleWhen?: unknown } — the first thing the new test project reported.

The case was also vacuous: the planner never reads disabledWhen, so the assertion { edit: true, count: 1 } holds identically for editPredicates: {}. It pinned "an object with no visibleWhen does not hide the item", which is worth pinning, but not what its name claims.

PR (tranche 4, components) annotates the fixture with DataTableSchema['rowEditPredicates'] — derived, not hand-copied — so the same value passes the way it does in production, and states in a comment which key is being ignored. The case name is left alone deliberately rather than quietly renamed.

The decision, when someone takes it

Either (a) widen the planner's parameter to the declared pair so the three restatements collapse to one, accepting that the function still reads only visibleWhen; or (b) keep it consumption-shaped and derive it — Pick< NonNullable< DataTableSchema['rowEditPredicates'] >, 'visibleWhen' > — so it cannot drift from the authoring type it is a subset of. Either beats a third hand-written copy. Also worth checking while there: whether the disabled-item counting behaviour the vacuous case was reaching for is pinned anywhere that can actually observe it.

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM + RULING — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4354-row-menu-predicate-derive, one dev agent, one PR, Fixes #4354. finding → pm:queue.

    Ruling (delegated, veto window open): option (b) — the planner's parameter stays consumption-shaped but becomes DERIVED: Pick<NonNullable<DataTableSchema['rowEditPredicates']>, 'visibleWhen'> (and the delete twin). This is the derivation-over-enumeration doctrine's exact shape (the #4277/#4297 precedent): the signature keeps telling the truth about what the function reads while becoming structurally unable to drift from the authoring type it subsets. Option (a) would have the signature claim a read that doesn't happen.

    The rider rides: measure whether the disabled-item counting behavior (what the vacuous case reached for) is pinned anywhere that can OBSERVE it — through the rendered row-menu item, not the planner's return. If unpinned, add the observable pin; then the vacuous case either becomes that pin or gets renamed to what it actually asserts (tranche 4's comment already names the ignored key — finish that thought rather than leaving the misleading name).


    Generated by Claude Code

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