NavigationMenuItem's `ui` type requires every slot key instead of allowing partial overrides
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 6.9k
- Forks
- 1.1k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 57
Description
Environment
- Package Manager:
npm @nuxt/ui:4.11.1vue:3.5.43- Vue + Inertia.js (no Nuxt), using
@nuxt/ui/vitewithrouter: "inertia" - Type-checked with
vue-tsc --noEmit
Is this bug related to Nuxt or Vue?
Vue
Package
v4.x
Version
v4.11.1
Reproduction
No external sandbox — this is a pure type-definition issue, fully reproducible from the shipped .d.ts alone:
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
const items = computed<NavigationMenuItem[]>(() => [
{
label: 'Feed',
to: '/feed',
active: true,
// Type error: missing 'label', 'link', 'content', 'item', and 18 more
ui: { linkLeadingIcon: 'text-court-950' },
},
])
</script>
Description
NavigationMenuItem['ui'] is typed as a Pick over the component's generated slots type:
// node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue.d.ts
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLeadingChipSize' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childLabel' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>;
NavigationMenu['slots'] resolves to an index-signature type ({ [x: string]: SlotClass }), not an interface with individually optional keys. Picking specific literal keys off an index-signature type produces an object type where every picked key is required, since index signatures carry no per-property optionality for Pick to preserve.
The result: although ui itself is optional on NavigationMenuItem, supplying any ui object requires values for all ~21 picked slot keys — even though at runtime tv() merges partial slot overrides just fine. Overriding a single slot like linkLeadingIcon per-item (a use case asked about in #3683) is a TypeScript error.
Expected behavior: overriding a single slot on an item's ui prop should type-check, matching the runtime merge behavior — e.g. by wrapping the Pick result in Partial<...>:
ui?: Partial<Pick<NavigationMenu['slots'], /* ... */>>;
Current workaround: cast the object to the field's type to bypass the over-strict check:
ui: { linkLeadingIcon: 'text-court-950' } as NavigationMenuItem['ui'],
Additional context
The same Pick-based pattern (and likely the same issue) appears on other components' per-item ui fields, e.g. NavigationMenuChildItem.
Logs
error TS2322: Type '{ label: string; to: string; active: true; ui: { linkLeadingIcon: string; }; }' is not assignable to type 'NavigationMenuItem'.
Types of property 'ui' are incompatible.
Type '{ linkLeadingIcon: string; }' is missing the following properties from type
'Pick<{ [x: string]: SlotClass; }, "label" | "link" | "linkLeadingIcon" | "content" | "item" | ... 14 more ... | "childLinkDescription">':
label, link, content, item, and 18 more.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue.d.ts and reproduce the issue using vue-tsc --noEmit with the provided NavigationMenuItem example. Check the related NavigationMenuChildItem declaration as well. Done means a single-slot ui override type-checks without a cast, while the existing NavigationMenuItem fields remain valid.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nuxt, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100