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:130 — trigger: [{ type: 'button', label: 'Menu', variant: 'outline' }], an array |
SchemaNode itself does not admit arrays: packages/types/src/base.ts:398 — export 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
Found while implementing #7073. Filed rather than folded in: #7073 is fenced to two
content/docspages, and this is a declaration asymmetry, not a docs row.Measured on
origin/main592acafbeed97c5aed81a18f4d848b6179d3c044.The four spellings of one key
packages/types/src/overlay.ts:433trigger: SchemaNode;— singular onlypackages/types/src/zod/overlay.zod.ts:176trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])— singular or arrayoverlay.ts:486ContextMenuSchema.trigger?: SchemaNode | SchemaNode[]— singular or arraypackages/components/src/renderers/overlay/dropdown-menu.tsx:130—trigger: [{ type: 'button', label: 'Menu', variant: 'outline' }], an arraySchemaNodeitself does not admit arrays:packages/types/src/base.ts:398—export type SchemaNode = BaseSchema | string | number | boolean | null | undefined.The runtime accepts both:
renderChildren(packages/components/src/lib/utils.tsx:23-31) has an explicitArray.isArraybranch, and unwraps a single-element array for Radix'sasChild.Why no gate catches it
packages/types/src/__tests__/zod-mirror-parity.test.tsasserts 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
defaultPropsships — so copying the renderer's default into a typed document is a type error against the type that shipped it.Options: (a) widen
DropdownMenuSchema.triggertoSchemaNode | SchemaNode[], matchingContextMenuSchema, the mirror and the runtime — one-line, and makes the four spellings one; (b) narrow the mirror and thedefaultPropsto singular, if a single trigger node is the intended contract — butrenderChildren's array branch andContextMenuSchemaboth 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,HoverCardSchemaandSheetSchema, which also carry atrigger; I did not measure those.Refs: #7073 · #6150 · #5684.
Generated by Claude Code