storybook: Bug: Type support for Vue component slots in stories

Is your feature request related to a problem? Please describe

Storybook 7 introduces many improvements for type support when using Vue. Still, we’re seeing errors when trying to pass arguments to component slots.

Below, I’m making stories for my button component. Instead of passing text (content) to a label prop, I want my button component to behave like a native HTML element. The button component has a single, unnamed slot (default) where the content goes. The correct way to set up the default slot is with args/argTypes:

// Button.stories.ts

import { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  render: (args) => ({
    components: {
      Button,
    },
    setup() {
      return { args }
    },
    template: `
      <Button v-bind="args">{{ args.default }}</Button>
    `,
  }),
  argTypes: {
    default: {
      control: 'text',
    },
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
    },
  },
  args: {
    default: 'Click me',
    size: 'medium',
  },
}
export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const LongText: Story = {
  args: {
    default: 'I am a button with faaaar toooo much text',
  },
}

The code runs correctly and the story controls allows me to change the button text, but TypeScript is not happy.

Describe the solution you’d like

Add type support for Vue component slots, both default slot and named.

The BaseAnnotations type, which is extended by the ComponentAnnotations interface, holds the type definitions for args (Partial<TArgs>) and argTypes (Partial<ArgTypes<TArgs>>). From what I’ve been able to find, this seems like the right place to do the implementation.

TArgs is a mapped type by default, but is likely assigned only the component props. Then the implementation might be deeper than I’ve been able to look at this point.

Describe alternatives you’ve considered

The alternative is using @ts-ignore, but that is never a permanent solution.

Are you able to assist to bring the feature to reality?

yes, I can

Additional context

I’ve tried to solve this myself, but I’m not familiar enough to see where the actual changes needs to be made. I am however a TypeScript developer so if anyone could point me to the right file, I’d be happy to work out a solution.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 51 (32 by maintainers)

Most upvoted comments

Okay, I got something working on typelevel, will finalize tomorrow, and check if it also works on runtime:

<script setup lang="ts">
defineProps<{ otherProp: boolean; }>();
</script>

<template>
  <div class="container">
    <header>
      <slot name="header" title="Some title"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
test('Infer type of slots', () => {
  const meta = {
    component: BaseLayout,
  } satisfies Meta<typeof BaseLayout>;

  const Basic: StoryObj<typeof meta> = {
    args: {
      otherProp: true,
      default: () => 'Default',
      header: ({ title }) => `<h1>Some header with a ${title}</h1>`,
      footer: () => 'Footer',
    },
  };

  type Props = {
    readonly otherProp: boolean;
    header?: (_: { title: string }) => any;
    default?: (_: {}) => any;
    footer?: (_: {}) => any;
  };

  type Expected = StoryAnnotations<VueRenderer, Props, Props>;
  expectTypeOf(Basic).toEqualTypeOf<Expected>();
});

@chakAs3 Maybe we can pair up on this?

@CasperSocio As a workaround you can do:

type Story = StoryObj<typeof meta> & { default: string; sectionAside: string }

I’m not 100% sure how slots in Vue work, and what makes sense here. Those slots args are only used if there is a custom render function right? Does it make sense to show autocompletion for the slots, even when they are not referenced in the render function?

And what if a user wants to do something differently, then use the named slots:

  render: (args) => ({
    components: {
      Select,
      Option,
    },
    setup() {
      return { args }
    },
    template: `
      <Select v-bind="args">
        <Option>{{ args.option1 }}</Option>
        <Option>{{ args.option2 }}</Option>
      </Select>
    `,
  }),

What would really make this rock is to not have to write a render function in order to get this to work (similar to the auto-render functions provided for React). 😁

Sorry for missing this issue, I noticed that PR now uses vue-tsc to generate component types, volar v1.3 adds a new vue-component-type-helpers package, which is used behind vue-component-meta and can be used to extract The component type generated by vue-tsc, this may also apply to storybook.

Please note that v1.3 is a pre-release version of v1.4, there are still some unresolved edge cases, you can also wait for v1.4.

I really apriciate the work put into this bug guys! TS is happy again in 7.0.0-rc.0 and only throws these while building SB:

[vite:dts] Start generate declaration files...
src/components/Button/Button.stories.ts:20:5 - error TS2322: Type '{ default: { control: string; }; icon: { control: string; options: readonly ["add-circle-outline", "add-circle", "add", "arrow-down", "arrow-left", "arrow-right", "arrow-up", "check-circle-outline", ... 21 more ..., "upload"]; }; ... 5 more ...; onClick: { ...; }; }' is not assignable to type 'Partial<ArgTypes<Readonly<ExtractPropTypes<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...'.
  Object literal may only specify known properties, and 'default' does not exist in type 'Partial<ArgTypes<Readonly<ExtractPropTypes<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...'.

20     default: {
       ~~~~~~~~~~
21       control: 'text',
   ~~~~~~~~~~~~~~~~~~~~~~
22     },
   ~~~~~

  node_modules/@storybook/types/dist/index.d.ts:1153:5
    1153     argTypes?: Partial<ArgTypes<TArgs>>;
             ~~~~~~~~
    The expected type comes from property 'argTypes' which is declared here on type 'Meta<DefineComponent<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...; }; }, ... 10 more...'
src/components/Button/Button.stories.ts:51:5 - error TS2322: Type '{ default: string; iconPosition: "left"; shape: "pill"; size: "medium"; type: "button"; variant: "secondary"; }' is not assignable to type 'Partial<Readonly<ExtractPropTypes<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...; }; }...'.
  Object literal may only specify known properties, and 'default' does not exist in type 'Partial<Readonly<ExtractPropTypes<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...; }; }...'.

51     default: 'Click me',
       ~~~~~~~~~~~~~~~~~~~

  node_modules/@storybook/types/dist/index.d.ts:1148:5
    1148     args?: Partial<TArgs>;
             ~~~~
    The expected type comes from property 'args' which is declared here on type 'Meta<DefineComponent<{ icon: { type: PropType<"add-circle-outline" | "add-circle" | "add" | "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-circle-outline" | "check-circle" | "check" | ... 21 more ... | undefined>; required: false; default: null; }; ... 4 more ...; variant: { ...; }; }, ... 10 more...'
src/components/Button/Button.stories.ts:76:5 - error TS2322: Type '{ default: string; icon: "arrow-right"; iconPosition: "right"; }' is not assignable to type 'Partial<{ readonly type: "button" | "reset" | "submit" | undefined; readonly size: "small" | "large" | "medium" | undefined; readonly variant: "primary" | "secondary" | undefined; readonly icon: "add-circle-outline" | ... 30 more ... | undefined; readonly iconPosition: "left" | ... 1 more ... | undefined; readonly s...'.
  Object literal may only specify known properties, and 'default' does not exist in type 'Partial<{ readonly type: "button" | "reset" | "submit" | undefined; readonly size: "small" | "large" | "medium" | undefined; readonly variant: "primary" | "secondary" | undefined; readonly icon: "add-circle-outline" | ... 30 more ... | undefined; readonly iconPosition: "left" | ... 1 more ... | undefined; readonly s...'.

76     default: 'Read more',
       ~~~~~~~~~~~~~~~~~~~~

  node_modules/@storybook/types/dist/index.d.ts:1148:5
    1148     args?: Partial<TArgs>;
             ~~~~
    The expected type comes from property 'args' which is declared here on type 'StoryAnnotations<VueRenderer, { readonly type: "button" | "reset" | "submit" | undefined; readonly size: "small" | "large" | "medium" | undefined; readonly variant: "primary" | "secondary" | undefined; readonly icon: "add-circle-outline" | ... 30 more ... | undefined; readonly iconPosition: "left" | ... 1 more ... |...'

Using vite v4.1.4 and vite-plugin-dts 2.1.0, which is throwing the errors, but it doesn’t prevent the build from completing. Nor does it impact the SB instance I’m hosting om Firebase. Just thought you should know that there’s probably missing type exports 😅

Again: Thanks guys, stories are going to be a lot easier to write now

@kasperpeulen i guess there is some bugs but no worries i’m already reworking the renderer render in reactive way i’m will be checking this and fix the bug i will add you to review the PR if you don’t mind

@chakAs3 I think we want to only close tickets when we do a new release cc @shilman

Nice! So we can get the type def like this:

image

if you component has slots it will be shown in the control in SLOTS section , see here i have 2 default and icon and i can passe as JSON format

In my example at the top, you’ll find the *.stories.ts file that yields the slot section of the controls. The code will work as intended, but TS still throws a type error Object literal may only specify known properties, and 'default' does not exist in type 'Partial<Readonly<ExtractPropTypes....

meta.args and meta.argTypes must be able to contain the component slots as props. If the component has an unnamed slot, we also need to have meta.argTypes.default. And if the component has a sectionAside slot, we also need meta.argTypes.sectionAside.

When this is done, we should also improve the documentation for how to use slots in stories.

Write stories / Args / Args can modify any aspect of your component contains information on how to use slots with Vue and TS, but the example can definitely be improved once we have proper type support.