
Storybook 7 中改进的类型安全性
CSF3 语法与 TypeScript satisfies 结合,为您提供更严格的类型和改进的开发者体验
使用 TypeScript 编写代码可以提高您的生产力并使您的代码更健壮。您会收到类型检查警告、自动完成,并且 Storybook 会推断类型以自动生成 ArgsTable。它还有助于在编码时检测错误和边缘情况。
自 6.0 版本以来,Storybook 提供了内置的零配置 TypeScript 支持。这提供了出色的自动完成体验,但遗憾的是,它不会警告您缺少必需的 args。
我很高兴地分享 Storybook 7 为 stories 提供了增强的类型安全性。这得益于 Component Story Format(CSF) 3 和新的 TypeScript (4.9+) satisfies
运算符的结合。
- 💪 更严格的 story 类型
- ⌨️ 更好的编辑器内类型检查
- 🌐 Meta 和 Story 对象已链接,以推断组件级别的 args
- 🎮 自动检测 Action args
- 🤖 用于轻松升级的 Codemod
为什么需要新的类型?
Storybook 6 中的 TypeScript 类型在自动完成方面表现良好。但是,它们并非完全类型安全。如果您忘记提供 prop,TypeScript 应该在您的编辑器中发出警告;不幸的是,您只能在运行时收到 TypeError。


喜欢类型安全的人(像我一样!),一直通过完全不使用 Storybook args 约定来规避这个问题,如下所示
const Primary: Story<ButtonProps> = () => (
<Button disabled label="Label" />
);
它可以工作,但是如果您想使用 controls,则必须使用 args
,因为 Storybook 需要初始值才能在控制面板中显示它,然后根据用户输入动态覆盖它。此外,此语法需要更多重复,因为您必须跨 stories 重复模板。

介绍 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 特定的 ComponentMeta
和 ComponentStory
实用程序。
与 satisfies
结合使用以获得更好的类型安全性
如果您使用的是 TypeScript 4.9+,则可以利用新的 satisfies 运算符以获得更严格的类型检查。为了说明这一点,让我们看一个例子。
考虑这个 Button 组件。您会注意到,在 Primary story(左侧)中,我们没有指定必需的 label
arg。TypeScript 应该引发关于此的错误,但事实并非如此。

让我们使用 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 错误

自动推断组件级别的 args
即使在我们的 meta 级别 args
中提供了 label
,TypeScript 仍然在我们的 stories 上显示错误。那是因为 TypeScript 不知道 CSF 它们之间的连接。所以让我们告诉它!将 typeof meta
传递给 StoryObj
,以便 TypeScript 理解 args 可以在 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 语言特性 (Volar) 和 TypeScript Vue 插件 扩展在 VSCode 中获得编辑器支持。
此设置将为您的 .stories.ts
文件添加对 *.vue
导入的类型支持,从而提供相同的类型安全性和自动完成功能。

Svelte
Svelte 还为 .svelte
文件提供了出色的 TypeScript 支持。例如,考虑以下组件。您可以使用 svelte-check
运行类型检查,并使用 Svelte for VSCode 扩展 添加 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 文件,提供类型安全性和自动完成功能。

Angular 和 Web Components
我们尚无法在使用 Angular 和 Web components 的 satisfies
运算符时提供额外的类型安全性。它们都利用类加装饰器方法。装饰器提供运行时元数据,但不提供编译时元数据。
因此,似乎无法确定类中的属性是必需属性还是可选属性(但由于默认值而不可为空)还是不可为空的内部状态变量。
有关更多信息,请参阅以下讨论:github.com/storybookjs/storybook/discussions/20988
立即尝试
新的 Meta
和 StoryObj
类型现在已在 Storybook 7 beta 中提供;请参阅我们的 SB7 迁移指南。升级项目后,您可以运行迁移使用
npx storybook migrate upgrade-deprecated-types --glob="**/*.stories.@(js|jsx|ts|tsx)"
请注意,此 codemod 不会自动添加 satisfies
运算符。我们计划很快发布第二个 codemod,这将使 TypeScript 4.9+ 的用户能够迁移到新类型并添加 satisfies
运算符。请留意未来几周的更新。
下一步是什么?
我们很高兴 TypeScript 4.9 更新最终解决了长期存在的问题,即未收到关于缺少必需参数的警告。satisfies
运算符与 StoryObj
类型结合使用,使 TypeScript 能够理解组件和 story 级别参数之间的连接。因此,它现在更擅长显示这些警告。
我们希望尽快发布更多改进。例如,我们正在探索基于已安装的插件改进 story 参数类型的方法。此外,我们正在寻求改进 argTypes
的类型定义。
长期以来,Storybook 一直提供内置的 TypeScript 支持。但是,stories 不是强类型的。
— Storybook (@storybookjs) 2023 年 2 月 8 日
您在 story 文件中获得了出色的自动完成功能,但没有关于缺少 args 的警告。
Storybook 7 通过结合 CSF3 和 TS satisfies 运算符解决了这个问题 » https://#/eF1NJER00o pic.twitter.com/Qc5sJRFWDF