Back to blog

Improved type safety in Storybook 7

CSF3 syntax combined with TypeScript satisfies gives you stricter types and an improved developer experience

loading
Kasper Peulen
โ€” @KasperPeulen
Last updated:

Writing in TypeScript makes you more productive and your code more robust. You get type check warnings, autocomplete, and Storybook infers types to generate an ArgsTable automatically. It also helps detect bugs and edge cases while coding.

Storybook has provided built-in zero config TypeScript support since 6.0. That gives a great autocomplete experience, but sadly it didnโ€™t warn you about missing required args.

I'm thrilled to share that Storybook 7 provides enhanced type safety for stories. This was made possible with the combination of Component Story Format(CSF) 3 and the new TypeScript (4.9+) satisfies operator.

  • ๐Ÿ’ช Stricter story types
  • โŒจ๏ธ Better in-editor type checking
  • ๐ŸŒ Meta and Story objects are linked to infer component level args
  • ๐ŸŽฎ Action args are automatically detected
  • ๐Ÿค– Codemod for easy upgrades

Why new types?

The TypeScript types in Storybook 6 work well for autocompletion. However, they are not completely type-safe. If you forget to provide a prop, TypeScript should alert you in your editor; unfortunately, you only got the TypeError at runtime.

Folks who prefer type safety (like me!), have been circumventing this issue by not using the Storybook args convention altogether, like so:

const Primary: Story<ButtonProps> = () => (
  <Button disabled label="Label" />
);

It works, but if you want to use controls, you must use args as Storybook needs the initial value to display it in the control panel and then dynamically override it based on user input. Additionally, this syntax requires more duplication, as you have to repeat the template across stories.

controls.png

Introducing the StoryObj type

CSF3 enables you to manipulate stories as objects. To facilitate this, we have also created a StoryObj type, which automatically infers the type of component props.

Provide the component type (e.g., typeof Button) as a generic parameter. Alternatively, if your renderer is class-basedโ€”such as Svelte, Angular, or Web Componentsโ€”you can just use the component itself (e.g., Button).

Hereโ€™s a side-by-side comparison:

The previous Story type wasn't powerful enough to automatically infer prop types, so you had to manually specify them. Additionally, React components often don't export a type for their props, so you had to rely on the React-specific ComponentStory type.

However, this new syntax works for React, Vue, Svelte, Angular, and Web Components! Therefore, we're deprecating the React-specific ComponentMeta and ComponentStory utilities in Storybook 7.

Pair it with satisfies for better type safety

If you are using TypeScript 4.9+, you can take advantage of the new satisfies operator to get stricter type checking. To illustrate this, let's look at an example.

Consider this Button component. You'll notice, in the Primary story (on the left), that we haven't specified the required label arg. TypeScript should be raising an error about that, but it isn't.

without-satisfies.png

Let's use satisfies operator to fix this issue.

// Button.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
  title: 'Example/Button',
  component: Button,
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    primary: true,
  },
};

Now if you forget to provide a required arg, youโ€˜ll get a TypeScript error:

with-satisfies.png

Auto-infer component level args

Even with label provided in our meta-level args, TypeScript was showing errors on our stories. Thatโ€™s because TypeScript doesnโ€™t know about CSFโ€™s connection between them. So letโ€™s tell it! Pass typeof meta to StoryObj so that TypeScript understands the args can be defined both at the story and meta level, and the errors are go away.

// Button.stories.tsx

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta = {
  title: 'Example/Button',
  component: Button,
  args: {
    label: 'Default',
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// ๐Ÿ‘‡ TS won't complain about "label" missing
export const Primary: Story = {
  args: {
    primary: true,
  },
};

const Secondary: Story = {
  args: { disabled: false }
};

const Disabled: Story = {
  args: { disabled: true }
};

We also recommend that you extract StoryObj<typeof meta> to a type, so that you can reuse it across all stories in a file. For example, we use it to type the Primary, Secondary and Disabled stories above.

Putting it all together

Here's a complete example. Notice that we're using the satisfies pattern for both the meta and story levels. This helps us maintain type safety when sharing a play function across stories.

When sharing a play function across stories, TypeScript will throw an error by default, as the play function may be undefined. However, the satisfies operator enables TypeScript to infer whether the play is defined or not.

import type { Meta, StoryObj } from '@storybook/react';
import { screen, userEvent } from '@storybook/testing-library';
import { AccountForm } from './AccountForm';

const meta = {
  component: AccountForm,
} satisfies Meta<typeof AccountForm>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Standard = {
  args: {
    passwordVerification: false,
  },
} satisfies Story;

export const StandardEmailFilled = {
  ...Standard,
  play: async () => {
    await userEvent.type(
      await screen.findByLabelText('Email'),
      'marcus@acme.com'
    );
  },
} satisfies Story;

export const VerificationSuccess = {
  args: {
    passwordVerification: true,
  },
  play: async () => {
    // ๐Ÿ‘‡ Reuse play function from previous story
    await StandardEmailFilled.play();

    await userEvent.type(
      await screen.findByLabelText('Password'),
      'j129ks#82p23o'
    );
    await userEvent.type(
      await screen.findByLabelText('Verify Password'),
      'j129ks#82p23o'
    );
    await userEvent.click(
      await screen.findByRole('button', { name: /submit/i })
    );
  },
} satisfies Story;

Framework specific tips

Template-based frameworks such as Vue and Svelte typically require editor extensions to enable syntax highlighting, autocomplete, and type checking. Here are a few tips to help you set up the ideal environment for them.

Vue

Vue has excellent support for TypeScript, and we have done our utmost to take advantage of that in the stories files. For example, consider the following strongly typed Vue3 single file component (SFC):

<script setup lang="ts">
defineProps<{ count: number, disabled: boolean }>()

const emit = defineEmits<{
  (e: 'increaseBy', amount: number): void;
  (e: 'decreaseBy', amount: number): void;
}>();
</script>

<template>
  <div class="card">
    {{ count }}
    <button @click="emit('increaseBy', 1)" :disabled='disabled'>
        Increase by 1
    </button>
    <button @click="$emit('decreaseBy', 1)" :disabled='disabled'>
        Decrease by 1
    </button> 
  </div>
</template>

You can type check SFC files with vue-tsc and get editor support in VSCode by installing the Vue Language Features (Volar) and TypeScript Vue Plugin extensions.

This setup will add type support for *.vue imports to your .stories.ts files, providing the same type safety and autocomplete features.

vue-ts.png

Svelte

Svelte also offers excellent TypeScript support for .svelte files. For example, consider the following component. You can run type checks using svelte-check and add VSCode editor support with the Svelte for VSCode extension.

<script lang="ts">
  import { createEventDispatcher } from 'svelte';

  export let count: number;
  export let disabled: boolean;

  const dispatch = createEventDispatcher();
</script>

<div class="card">
  {count}
  <button on:click={() => dispatch('increaseBy', 1)} {disabled}> Increase by 1 </button>
  <button on:click={() => dispatch('decreaseBy', 1)} {disabled}> Decrease by 1 </button>
</div>

The same setup works with Svelte stories files too, providing both type safety and autocompletion.

svelte-ts.png

Angular and Web Components

We are not yet able to provide additional type safety using the satisfies operator with Angular and Web components. They both utilize a class plus decorator approach. The decorators provide runtime metadata, but do not offer metadata at compile time.

As a result, it appears impossible to determine if a property in the class is a required property or an optional property (but non-nullable due to a default value) or a non-nullable internal state variable.

For more information, please refer to the following discussion: github.com/storybookjs/storybook/discussions/20988

Try it today

The new Meta and StoryObj types are now available in the Storybook 7 beta; see our SB7 migration guide. After your project is upgraded, you can run a migration using:

npx storybook migrate upgrade-deprecated-types --glob="**/*.stories.@(js|jsx|ts|tsx)"

Note, this codemod does not add the satisfies operator automatically. We plan to release a second codemod soon, which will enable users of TypeScript 4.9+ to migrate to the new types and add in the satisfies operator. Look out for that update in the coming weeks.

What's next?

We are delighted that the TypeScript 4.9 update finally resolves the long-standing issue of not receiving warnings about missing required arguments. The satisfies operator in combination with the StoryObj type enables TypeScript to understand the connection between component and story level arguments. As a result, it is now better at displaying these warnings.

We hope to ship more improvements soon. For instance, we are exploring ways to improve the typing of story parameters based on the addons installed. Additionally, we are looking to improve the type definitions of argTypes.

Join the Storybook mailing list

Get the latest news, updates and releases

6,616 developers and counting

Weโ€™re hiring!

Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.

View jobs

Popular posts

Storybook 7 Docs

New architecture, streamlined UX, and readymade doc blocks
loading
Tom Coleman

Storybook for SvelteKit

Zero-config support for SvelteKit 1.0 with our new framework
loading
Jeppe Reinhold

Component Story Format 3 is here

Next gen story format to make you more productive
loading
Michael Shilman
Join the community
6,616 developers and counting
WhyWhy StorybookComponent-driven UI
DocsGuidesTutorialsChangelogTelemetry
CommunityAddonsGet involvedBlog
ShowcaseExploreProjectsComponent glossary
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI