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
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
noImplicitAnywent on, because the props type never reached the code that reads it (see the sibling finding on thePropsWithoutRefcollapse).What
@object-ui/typesexports two action types:ActionSchema— fromcrud.ts, carrying its own@deprecated Use UIActionSchema ... for new codeUIActionSchema— fromui-action.ts, exported under that alias, the modern oneaction-bar.tsx,action-menu.tsxandaction-group.tsximport the legacy one and declare their authoring keys with it:…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):
ActionSchemacannot be itactionRendersAt(a, schema.location)— the shared placement predicate, which takes{ locations?: readonly string[] }locationsat all →TS2559(weak-type detection), inaction-barandaction-groupa.variant === 'primary'variantis'default' | 'outline' | 'ghost' | 'link'→TS2367, three timesInlineActionButton,DropdownActionItem,ActionMenuItem,ActionAutoTrigger)typeis the literal'action'; the values carry'form' | 'script' | 'url' | 'flow' | 'api' | 'modal'→TS2322action-bar.tsx's own documented example, at the top of the file, is aUIActionSchema(type: 'script'). Andaction-group.tsx/action-menu.tsxalready carry(action.variant as string)casts that exist for no other reason than to get around the legacy union.Why it was invisible
schemanever arrived typed. These areforwardRefcomponents whose props type carries[key: string]: any, andforwardRefroutes props throughPropsWithoutRef, whoseOmitcollapses such a type to the bare index signature — soschemawasany,schema.actionswasany, 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 theactions?: ActionSchema[]declarations on the legacy type, naming the mismatch in a comment at each site. Reconciling the declarations reaches roughly 46ActionSchemareferences across 12 files in this package, and it is a contract decision rather than a consequence of turning a compiler flag on:action:*schema interfaces publish the modern action type (and every consumer constructing one moves with them), orNeither of those is settled by #4353's ruling, which is why #4417 stopped at the boundary.
Notes
packages/components/src/index.ts, so no published declaration is involved either way — this is an internal-consistency question, not a breaking-change question.crud.ts'sActionSchemasays the interface "will be removed in a future major version", so the drift resolves itself destructively if nobody decides first.Generated by Claude Code