Skip to content

dynamic slots are untyped #23

@sandros94

Description

@sandros94

🐛 The bug

We found another bug while working on Nuxt UI, and this one is even harder to provide a native vue-sfc-transformer reproduction (in fact I'm not even able to make the linked one run at all).

Nuxt UI has a number of components that other than static slots (eg: label, content, leading, etc..) also provides dynamic slots, meaning that you can directly access a specific item in your input array.

For example using UTabs:

<script setup lang="ts"> import type { TabsItem } from '@nuxt/ui'  const tabs = [  {  label: 'Tab 1',  slot: 'tab1' as const,  content: 'Content 1'  },  {  label: 'Tab 2',  slot: 'tab2' as const,  content: 'Content 2'  },  {  label: 'Tab 3',  slot: 'tab3' as const,  content: 'Content 3'  } ] satisfies TabsItem[] </script> <template> <UTabs :items="tabs"> <template #tab1="{ item }"> {{ item.content }} </template> </UTabs> </template>

but once parsed with vue-sfc-transformer it no longer works, and item falls back to the default TabsItem type (screenshot from an actual project installing @nuxt/ui)

Image

🛠️ To reproduce

https://stackblitz.com/edit/github-sadsa3hd?file=index.js

🌈 Expected behaviour

should provide the following auto-complete and correctly type item (these screenshots are from within the @nuxt/ui playground)

Image

Image

ℹ️ Additional context

I'm not using the latest @nuxt/ui release as I was fixing other stuff related to dynamic slots, please use the latest CI https://pkg.pr.new/@nuxt/ui@9817465 (coming from nuxt/ui#3857)

I also tried directly accessing the types and they do seem to work (screenshot from an actual project installing @nuxt/ui)
Image

src

  • TabsSlots type

  • DynamicSlots utility

  • currently generated Tabs.vue.d.ts
    import type { VariantProps } from 'tailwind-variants'; import type { TabsRootProps, TabsRootEmits } from 'reka-ui'; import type { AvatarProps } from '../types'; import type { DynamicSlots, PartialString } from '../types/utils'; declare const tabs: import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, { color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, { color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, unknown, unknown, undefined>>>; export interface TabsItem { label?: string; /**  * @IconifyIcon  */ icon?: string; avatar?: AvatarProps; slot?: string; content?: string; /** A unique value for the tab item. Defaults to the index. */ value?: string | number; disabled?: boolean; [key: string]: any; } type TabsVariants = VariantProps<typeof tabs>; export interface TabsProps<T extends TabsItem = TabsItem> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> { /**  * The element or component this component should render as.  * @defaultValue 'div'  */ as?: any; items?: T[]; /**  * @defaultValue 'primary'  */ color?: TabsVariants['color']; /**  * @defaultValue 'pill'  */ variant?: TabsVariants['variant']; /**  * @defaultValue 'md'  */ size?: TabsVariants['size']; /**  * The orientation of the tabs.  * @defaultValue 'horizontal'  */ orientation?: TabsRootProps['orientation']; /**  * The content of the tabs, can be disabled to prevent rendering the content.  * @defaultValue true  */ content?: boolean; /**  * The key used to get the label from the item.  * @defaultValue 'label'  */ labelKey?: string; class?: any; ui?: PartialString<typeof tabs.slots>; } export interface TabsEmits extends TabsRootEmits<string | number> { } type SlotProps<T extends TabsItem> = (props: { item: T; index: number; }) => any; export type TabsSlots<T extends TabsItem = TabsItem> = { 'leading': SlotProps<T>; 'default': SlotProps<T>; 'trailing': SlotProps<T>; 'content': SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & DynamicSlots<T, undefined, { index: number; }>; declare const _default: <T extends TabsItem>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{ props: __VLS_PrettifyLocal<any & TabsProps<T> & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: Readonly<{ leading: SlotProps<T>; default: SlotProps<T>; trailing: SlotProps<T>; content: SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & { [K in NonNullable<T["slot"]> as K extends string ? K : never]: (props: { item: Extract<T, { slot: K; }>; } & { index: number; }) => any; } & { [key: string]: (props: { item: T; } & { index: number; }) => any; }> & { leading: SlotProps<T>; default: SlotProps<T>; trailing: SlotProps<T>; content: SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & { [K in NonNullable<T["slot"]> as K extends string ? K : never]: (props: { item: Extract<T, { slot: K; }>; } & { index: number; }) => any; } & { [key: string]: (props: { item: T; } & { index: number; }) => any; }; emit: (evt: "update:modelValue", payload: string | number) => void; }>) => import("vue").VNode & { __ctx?: Awaited<typeof __VLS_setup>; }; export default _default; type __VLS_PrettifyLocal<T> = { [K in keyof T]: T[K]; } & {};

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions