Skip to content

dynamic slots are untyped #23

Closed as not planned
Closed as not planned
@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