Skip to content

finding(components): the action renderers declare the deprecated ActionSchema but are written against UIActionSchema #4418

Description

@yinlianghui

Found while typing the action renderers for #4353 (PR #4417). Observation-class: nothing a user hits today, no fix proposed here. Unexercised drift — the contradiction was unverifiable until noImplicitAny went on, because the props type never reached the code that reads it (see the sibling finding on the PropsWithoutRef collapse).

What

@object-ui/types exports two action types:

  • ActionSchema — from crud.ts, carrying its own @deprecated Use UIActionSchema ... for new code
  • UIActionSchema — from ui-action.ts, exported under that alias, the modern one

action-bar.tsx, action-menu.tsx and action-group.tsx import the legacy one and declare their authoring keys with it:

export interface ActionBarSchema {
  actions?: ActionSchema[];        // legacy
  systemActions?: ActionSchema[];  // legacy
}

…while the code underneath is written against the modern one. Three independent proofs, each a compiler error the moment the legacy type is actually applied to those values (all four observed on the #4353 branch before the annotations were switched):

what the code does why legacy ActionSchema cannot be it
actionRendersAt(a, schema.location) — the shared placement predicate, which takes { locations?: readonly string[] } legacy has no locations at all → TS2559 (weak-type detection), in action-bar and action-group
the objectui#2339 ordering tie-break, a.variant === 'primary' legacy variant is 'default' | 'outline' | 'ghost' | 'link'TS2367, three times
every handoff to a leaf (InlineActionButton, DropdownActionItem, ActionMenuItem, ActionAutoTrigger) legacy type is the literal 'action'; the values carry 'form' | 'script' | 'url' | 'flow' | 'api' | 'modal'TS2322

action-bar.tsx's own documented example, at the top of the file, is a UIActionSchema (type: 'script'). And action-group.tsx / action-menu.tsx already carry (action.variant as string) casts that exist for no other reason than to get around the legacy union.

Why it was invisible

schema never arrived typed. These are forwardRef components whose props type carries [key: string]: any, and forwardRef routes props through PropsWithoutRef, whose Omit collapses such a type to the bare index signature — so schema was any, schema.actions was any, and the declaration above was never compared against anything. Filed separately as the mechanism.

What #4417 did and did not do

PR #4417 typed the implementation from UIActionSchema — that is what the values are — and left the actions?: ActionSchema[] declarations on the legacy type, naming the mismatch in a comment at each site. Reconciling the declarations reaches roughly 46 ActionSchema references across 12 files in this package, and it is a contract decision rather than a consequence of turning a compiler flag on:

  • do the three action:* schema interfaces publish the modern action type (and every consumer constructing one moves with them), or
  • is the legacy type genuinely the authoring contract here, in which case the implementation is reading keys the contract does not grant?

Neither of those is settled by #4353's ruling, which is why #4417 stopped at the boundary.

Notes

  • None of the three interfaces, nor the leaf components, is re-exported from packages/components/src/index.ts, so no published declaration is involved either way — this is an internal-consistency question, not a breaking-change question.
  • The deprecation on crud.ts's ActionSchema says the interface "will be removed in a future major version", so the drift resolves itself destructively if nobody decides first.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions