Skip to content

finding(types): DropdownMenuSchema.trigger declares a single SchemaNode while its zod mirror, its sibling ContextMenuSchema, and its own shipped defaultProps all use the array form #7081

Description

@claude

Found while implementing #7073. Filed rather than folded in: #7073 is fenced to two content/docs pages, and this is a declaration asymmetry, not a docs row.

Measured on origin/main 592acafbeed97c5aed81a18f4d848b6179d3c044.

The four spellings of one key

where what it says
packages/types/src/overlay.ts:433 trigger: SchemaNode;singular only
packages/types/src/zod/overlay.zod.ts:176 trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])singular or array
sibling, overlay.ts:486 ContextMenuSchema.trigger?: SchemaNode | SchemaNode[]singular or array
the renderer's own shipped default packages/components/src/renderers/overlay/dropdown-menu.tsx:130trigger: [{ type: 'button', label: 'Menu', variant: 'outline' }], an array

SchemaNode itself does not admit arrays: packages/types/src/base.ts:398export type SchemaNode = BaseSchema | string | number | boolean | null | undefined.

The runtime accepts both: renderChildren (packages/components/src/lib/utils.tsx:23-31) has an explicit Array.isArray branch, and unwraps a single-element array for Radix's asChild.

Why no gate catches it

packages/types/src/__tests__/zod-mirror-parity.test.ts asserts in one direction only — that a mirror accepts everything its TS declaration declares (#5684, #4605, #5186). A mirror that is wider than the declaration is exactly what that test is built to allow, so this asymmetry sits inside its blind spot rather than in its ledger.

Why it matters

The published TS type is the one an author's editor reads. Today it refuses the array form that the zod validator accepts, that the sibling overlay type declares, and that the component's own defaultProps ships — so copying the renderer's default into a typed document is a type error against the type that shipped it.

Options: (a) widen DropdownMenuSchema.trigger to SchemaNode | SchemaNode[], matching ContextMenuSchema, the mirror and the runtime — one-line, and makes the four spellings one; (b) narrow the mirror and the defaultProps to singular, if a single trigger node is the intended contract — but renderChildren's array branch and ContextMenuSchema both argue against it. (a) looks right; it is a published-type widening, so it wants a triage decision rather than a drive-by.

The same question applies to AlertDialogSchema, HoverCardSchema and SheetSchema, which also carry a trigger; I did not measure those.

Refs: #7073 · #6150 · #5684.


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

Labels

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions