返回博客

Storybook 7 中改进的类型安全

CSF3 语法与 TypeScript 结合提供了更严格的类型和改进的开发者体验

loading
Kasper Peulen
@KasperPeulen
最后更新

使用 TypeScript 编写代码可以提高你的生产力,并使代码更健壮。你可以获得类型检查警告、自动完成功能,Storybook 还可以自动推断类型来生成 ArgsTable。它还有助于在编码时检测 bug 和边界情况。

Storybook 从 6.0 版本开始提供了内置的零配置 TypeScript 支持。这提供了出色的自动完成体验,但遗憾的是,它并不会警告你缺少必需的参数。

我很高兴地宣布,**Storybook 7 为 stories 提供了增强的类型安全**。这是通过结合 Component Story Format (CSF) 3 和新的 TypeScript (4.9+) satisfies 操作符来实现的。

  • 💪 更严格的 story 类型
  • ⌨️ 更好的编辑器内类型检查
  • 🌐 Meta 和 Story 对象与组件级别参数关联,用于推断
  • 🎮 Action 参数自动检测
  • 🤖 Codemod 轻松升级

为什么需要新类型?

Storybook 6 中的 TypeScript 类型对于自动完成功能效果很好。但是,它们并不完全是类型安全的。如果你忘记提供一个 prop,TypeScript 应该会在你的编辑器中发出警告;不幸的是,你只能在运行时遇到 `TypeError`。

那些偏爱类型安全(比如我!)的人,一直通过不使用 Storybook 的 args 约定来规避这个问题,就像这样

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

这可以工作,但如果你想使用 controls,你必须使用 args,因为 Storybook 需要初始值才能在控件面板中显示它,然后根据用户输入动态覆盖它。此外,这种语法需要更多的重复,因为你必须在 stories 中重复 template。

controls.png

隆重推出 **StoryObj 类型**

CSF3 使你能够将 stories 作为对象进行操作。为了方便起见,我们还创建了一个 StoryObj 类型,它可以自动推断组件 props 的类型。

将组件类型(例如 typeof Button)作为泛型参数传递。或者,如果你的渲染器是基于类的——例如 Svelte、Angular 或 Web Components——你可以直接使用组件本身(例如 Button)。

这里是并排比较

之前的 Story 类型不够强大,无法自动推断 prop 类型,因此你必须手动指定它们。此外,React 组件通常不导出其 props 的类型,因此你必须依赖于 React 特定的 ComponentStory 类型。

然而,这种新语法适用于 React、Vue、Svelte、Angular 和 Web Components!因此,我们在 Storybook 7 中弃用了 React 特定的 ComponentMetaComponentStory 工具。

satisfies 配合使用以获得更好的类型安全性

如果你正在使用 TypeScript 4.9+,你可以利用新的 satisfies 操作符来获得更严格的类型检查。为了说明这一点,让我们看一个例子。

考虑这个 Button 组件。你会注意到,在 Primary story(左侧)中,我们没有指定必需的 label arg。TypeScript 应该会对此发出错误,但它并没有。

without-satisfies.png

让我们使用 satisfies 操作符来解决这个问题。

// 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,
  },
};

现在,如果你忘记提供必需的 arg,你将收到一个 TypeScript 错误

with-satisfies.png

自动推断组件级别的参数

即使在我们的 meta 级别的 args 中提供了 label,TypeScript 仍然在我们的 stories 中显示错误。这是因为 TypeScript 不知道 CSF 之间的这种关联。所以我们来告诉它!将 typeof meta 传递给 StoryObj,这样 TypeScript 就能理解参数可以在 story 和 meta 级别定义,并且错误就会消失。

// 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 }
};

我们还建议你将 StoryObj<typeof meta> 提取到一个类型中,这样你就可以在文件中的所有 stories 中重用它。例如,我们使用它来为上面的 Primary、Secondary 和 Disabled stories 设置类型。

将所有内容整合在一起

这是一个完整的例子。请注意,我们对 meta 和 story 级别都使用了 satisfies 模式。这有助于我们在跨 stories 共享 play 函数时保持类型安全。

当跨 stories 共享 play 函数时,TypeScript 默认会抛出错误,因为 play 函数可能未定义。但是,satisfies 操作符使 TypeScript 能够推断 play 是否已定义。

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;

特定于框架的技巧

基于模板的框架,如 Vue 和 Svelte,通常需要编辑器扩展才能启用语法高亮、自动完成和类型检查。这里有一些技巧可以帮助你为它们设置理想的环境。

Vue

Vue 在 TypeScript 支持方面表现出色,我们已尽最大努力在 stories 文件中利用这一点。例如,请看下面的强类型 Vue3 单文件组件 (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>

你可以使用 vue-tsc 对 SFC 文件进行类型检查,并通过安装 Vue Language Features (Volar)TypeScript Vue Plugin 扩展来获得 VSCode 中的编辑器支持。

此设置将为你的 .stories.ts 文件添加对 *.vue 导入的类型支持,提供相同的类型安全和自动完成功能。

vue-ts.png

Svelte

Svelte 也为 .svelte 文件提供了出色的 TypeScript 支持。例如,请看下面的组件。你可以使用 svelte-check 运行类型检查,并通过 Svelte for VSCode extension 在 VSCode 中添加编辑器支持。

<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>

同样的设置也适用于 Svelte stories 文件,提供类型安全和自动完成功能。

svelte-ts.png

Angular 和 Web Components

我们还无法使用 satisfies 操作符为 Angular 和 Web Components 提供额外的类型安全。它们都使用类加上装饰器的方法。装饰器提供运行时元数据,但不在编译时提供元数据。

因此,似乎无法确定类中的属性是必需属性,还是可选属性(但由于默认值而不可为空),还是不可为空的内部状态变量。

有关更多信息,请参阅以下讨论: github.com/storybookjs/storybook/discussions/20988

立即试用

新的 MetaStoryObj 类型现已在 Storybook 7 beta 中提供;请参阅我们的 SB7 迁移指南。在项目升级后,你可以运行迁移

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

请注意,此 codemod 不会自动添加 satisfies 操作符。我们计划很快发布第二个 codemod,该 codemod 将允许 TypeScript 4.9+ 的用户迁移到新类型并添加 satisfies 操作符。请在未来几周内关注此更新。

下一步是什么?

我们很高兴 TypeScript 4.9 更新终于解决了长期存在的问题,即无法收到关于缺少必需参数的警告。satisfies 操作符与 StoryObj 类型相结合,使 TypeScript 能够理解组件和 story 级别参数之间的关联。因此,它在显示这些警告方面做得更好。

我们希望很快能发布更多改进。例如,我们正在探索如何基于已安装的插件来改进 story 参数的类型。此外,我们正在改进 argTypes 的类型定义。

加入 Storybook 邮件列表

获取最新消息、更新和发布信息

7,468开发者及更多

我们正在招聘!

加入 Storybook 和 Chromatic 团队。构建被数十万开发人员在生产中使用的工具。远程优先。

查看职位

热门帖子

Storybook 7 文档

新架构、精简的用户体验以及现成的文档块
loading
Tom Coleman

Storybook for SvelteKit

使用我们的新框架为 SvelteKit 1.0 提供零配置支持
loading
Jeppe Reinhold

Component Story Format 3 现已推出

下一代故事格式,让您更具生产力
loading
Michael Shilman
加入社区
7,468开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索项目组件词汇表
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI