Skip to content

finding(types): two arity conventions for runtime handler slots - 11 slots take no event, 8 take one, and the arity-0 form cannot express preventDefault #7706

Description

@claude

Carved out of the contract review of PR #7700 (comment 5550273792, item 8), which raised it as a package-wide question rather than that PR's defect and therefore did not block on it. #7700 merged 2026-09-05T07:40:44Z as 8ad218d58; without this card the finding would survive only inside a review comment on a merged PR.

Two arity conventions for one kind of slot, measured on origin/main 8ad218d58

declaration arity
packages/types/src/base.ts:828 EventHandlers.onClick?: (event?: any) => void | Promise<void> takes the event
packages/types/src/form.ts:62 ButtonSchema.onClick?: () => void | Promise<void> takes nothing
packages/types/src/overlay.ts:160 AlertDialogSchema.onAction?: () => void (new, from #7700) takes nothing

Census over packages/types/src/*.ts (tests excluded): 11 handler slots declared on…?: () => …, across crud.ts 2, form.ts 2, overlay.ts 2, and one each in data-display.ts, layout.ts, objectql.ts, ui-action.ts, views.ts. 8 declared on…?: (event?: any) => … — 7 in base.ts, 1 in complex.ts. Both spellings are live and neither is rare.

The concrete consequence

An arity-0 slot refuses a handler that takes the event. For AlertDialogSchema.onAction that rules out Radix's own documented idiom on AlertDialog.Action — calling event.preventDefault() to keep the dialog open — because there is no parameter to call it on. The (event?: any) spelling accepts both shapes and is equally forwardable, so the arity-0 form is strictly the narrower of the two.

⛔ This is not a defect in #7700. That PR followed the convention its own neighbour uses (ButtonSchema.onClick), which is the right call for one card; the question is which convention the package should have, and that is nobody's single card until it is written down.

Explicitly NOT a finding here

The missing | Promise<void> on onAction compared with ButtonSchema.onClick looks like a second gap and is not one: TypeScript's return-type-void rule makes () => Promise<void> assignable to () => void, so an async handler is accepted either way. Measured and recorded so it is not re-raised.

What is owed

A ruling on which arity the package publishes for runtime slots, then a sweep. ⛔ Do not open this by changing one slot — a per-slot fix is what produced the split. Note also that these are objectui#6124 runtime slots: the Zod mirrors refuse them by name (handlerKeyRefusal), so the arity lives only on the TS face and no accept set moves either way.

⚠️ Clause-② on the implementation is yes if the sweep widens arity-0 slots to take an event — it changes what the published TS face accepts. Re-measure the census at dispatch time; packages/types is under heavy change and this reading is a snapshot.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions