
用 TypeScript 编写 Stories
了解如何为您的 Story 添加类型,使它们更容易编写且更健壮

在开发 UI 组件时,你需要记住很多细节——属性名称、样式、状态管理、事件处理程序等等。通过使用 TypeScript 构建组件,你可以更好地捕捉这些细节并自动化你的工作流程。你的代码编辑器将对代码进行类型检查并提供更好的自动完成,从而节省大量开发时间和精力。
当将 TypeScript 与 Storybook 结合使用时,你将获得相同的改进的人体工程学。这就是为什么 Microsoft、Github 和 Codecademy 等领先团队使用 TypeScript 来编写 stories。
本文将向你展示如何使用 TypeScript 编写 stories。它涵盖了从基础到最佳实践的所有内容,并附有 React、Angular 和 Vue 的代码示例。
为什么用 TypeScript 编写 stories?
用 TypeScript 编写 stories 可以提高你的生产力。你不必在文件之间跳转来查找组件属性。你的代码编辑器会提示你缺少必需的属性,甚至可以自动完成属性值。此外,Storybook 会推断这些组件类型以自动生成 ArgsTable。

使用 TypeScript 还可以使你的代码更健壮。在编写 stories 时,你会复制组件在应用程序中的使用方式。通过类型检查,你可以在编码时捕获 bug 和边缘情况。
Storybook 内置了 TypeScript 支持,因此你可以开始使用,无需任何配置。让我们深入探讨类型化 story 的具体细节。
使用 Meta 和 Story 工具类型键入 stories
编写 stories 时,有两个方面键入很有帮助。第一个是 组件元数据,它描述和配置组件及其 stories。在 CSF 文件中,这就是默认导出。第二个是 stories 本身。Storybook 为每个提供了名为 Meta 和 Story 的工具类型。以下是一些使用这些类型的基本示例
React
// Button.stories.tsx
import * as React from "react";
import { Meta, Story } from "@storybook/react";
import { Button } from "./Button";
export default {
component: Button,
} as Meta;
export const Primary: Story = (args) => <Button {...args} />;
Primary.args = {
label: "Button",
primary: true,
};对于像 Angular 和 Vue 这样的基于模板的框架,story 的结构会有所不同,但键入策略保持不变。
Angular
// Button.stories.ts
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Button } from "./button.component";
export default {
component: Button,
decorators: [
moduleMetadata({
declarations: [Button],
imports: [CommonModule],
}),
],
} as Meta;
export const Primary: Story = (args) => ({
props: args,
template: `<app-button></app-button>`,
});
Primary.args = {
label: "Button",
primary: true,
};Vue 3
// Button.stories.ts
import { Meta, Story } from "@storybook/vue3";
import Button from "./Button.vue";
export default {
component: Button,
} as Meta;
export const Primary: Story = (args) => ({
components: { Button },
setup() {
return { args };
},
template: `<Button v-bind="args" />`,
});
Primary.args = {
primary: true,
label: "Button",
};启用更具体的类型检查
Meta 和 Story 类型都是 泛型,因此你可以为它们提供一个属性类型参数。通过这样做,TypeScript 将阻止你定义无效属性,并且所有 装饰器、play 函数或 loader 都将得到更全面的类型。
// Button.stories.ts
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Button } from "./button.component";
export default {
component: Button,
decorators: [
moduleMetadata({
declarations: [Button],
imports: [CommonModule],
}),
],
} as Meta<Button>;
export const Primary: Story<Button> = (args) => ({
props: args,
});
Primary.args = {
label: "Button",
primary: true,
size: "xl",
// ^ TypeScript error: type of `Button` does not contain `size`
};上面的代码将显示一个 TypeScript 错误,因为 Button 不支持 size 输入
键入模板 story 函数
通常会 提取一个模板函数以在多个 stories 中共享。例如,上面代码片段中的 story 可以这样写
const Template: Story<Button> = (args) => ({
props: args,
});
export const Primary = Template.bind({});
Primary.args = {
label: "Primary Button",
primary: true,
};
export const Secondary = Template.bind({});
Secondary.args = {
label: "Secondary Button",
primary: false,
};在此代码片段中,Primary 和 Secondary stories 正在克隆 Template 函数。通过启用 strictBindCallApply TypeScript 选项,你的 stories 可以自动继承 Template 的类型。换句话说,你不必在每个 story 上重新声明类型。你可以在 tsconfig 中启用此选项。
键入自定义 args
有时 stories 需要定义不包含在组件属性中的 args。例如,List 组件可以包含 ListItem 组件,你可以为渲染的 ListItem 数量提供一个 控件。
在这种情况下,你可以使用 交叉类型来扩展你为 args 提供的类型变量
// List.stories.ts
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { List } from "./list.component";
import { ListItem } from "./list-item.component";
export default {
component: List,
subcomponents: { ListItem },
decorators: [
moduleMetadata({
declarations: [List, ListItem],
imports: [CommonModule],
}),
],
} as Meta<List>;
// Expand Story’s type variable with `& { numberOfItems: number }`
export const NumberOfItems: Story<List & { numberOfItems: number }> = ({
numberOfItems,
...args
}) => {
// Generate an array of item labels, with length equal to the numberOfItems
const itemLabels = [...Array(numberOfItems)].map(
(_, index) => `Item ${index + 1}`
);
return {
// Pass the array of item labels to the template
props: { ...args, itemLabels },
// Iterate over those labels to render each ListItem
template: `<app-list>
<div *ngFor="let label of itemLabels">
<app-list-item [label]="label"></app-list-item>
</div>
</app-list>`,
};
};
NumberOfItems.args = {
numberOfItems: 1,
};
NumberOfItems.argTypes = {
numberOfItems: {
name: "Number of ListItems",
options: [1, 2, 3, 4, 5],
control: { type: "inline-radio" },
},
};
特定于 React 的工具类型
React 组件通常不导出其属性的类型。因此,Storybook for React 公开了 ComponentMeta 和 ComponentStory 类型。它们等同于 Meta 和 Story 泛型类型,但可以从组件本身推断属性类型。
// Button.stories.tsx
import * as React from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Button } from "./Button";
export default {
component: Button,
} as ComponentMeta<typeof Button>;
export const Primary: ComponentStory<typeof Button> = (args) => (
<Button {...args} />
);
Primary.args = {
label: "Button",
primary: true,
};
这些工具使用 typeof 运算符来推断组件的属性类型。因此,它们不能用于处理自定义 args,如上一节所示。
相反,你可以使用 Meta 和 Story 类型以及 React 的 ComponentProps 工具。例如
// Button.stories.tsx
import * as React from "react";
import { Meta, Story } from "@storybook/react";
import { Button } from "./Button";
type ButtonProps = React.ComponentProps<typeof Button>;
export default {
component: Button,
} as Meta<ButtonProps>;
export const WithCustomArg: Story<ButtonProps & { customArg: number }> = (args) => (
<Button {...args} />
);
Primary.args = {
label: "Button",
primary: true,
customArg: 3,
};Storybook v7 中的更改
上面所有的代码片段都是用 CSF 2 格式编写的。 Storybook 6.3 引入了 CSF 3,这是一种更紧凑、更具组合性的编写 stories 的方式。这是重写为 CSF 3 的第一个代码片段
// Button.stories.ts
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { CommonModule } from "@angular/common";
import { Button } from './button.component';
export default {
component: Button,
decorators: [
moduleMetadata({
declarations: [Button],
imports: [CommonModule],
}),
],
} as Meta<Button>;
export const Primary: StoryObj<Button> = {
args: {
label: "Button",
primary: true,
},
};请注意 StoryObj 工具类型。在 Storybook 7.0 中,CSF 3 将成为默认设置,StoryObj 类型将被重命名为 Story(以匹配其默认性质),用于 CSF 2 的类型将从 Story 重命名为 StoryFn。
此外,ComponentStoryObj、ComponentStory 和 ComponentStoryFn 也有相应的更改。
最后,CSF 3 版本的类型仍然是泛型的,它们接受组件或其 args 的类型变量,具体取决于类型。
Storybook 7.0 的 文档已更新以反映这些更改。
总结
用 TypeScript 编写 stories 可以更轻松地开发更健壮的组件。你可以获得类型安全和错误检查、自动完成建议等。
Storybook 提供零配置的 TypeScript 支持。你可以 自定义此设置以更好地满足你的需求。此外,Storybook 文档中的所有代码片段都提供 JavaScript 和 TypeScript 版本。

如果你想继续讨论,请加入我们在 Storybook Discord 的 #typescript 频道。我会在那里见到你!
用 @typescript 编写 stories 可以提高你的生产力。
— Storybook (@storybookjs) 2022 年 8 月 4 日
你的 IDE 会在你编码时捕获错误,并且你会获得更好的自动完成!
查看我们的新指南,了解如何键入 stories。它涵盖了 Storybook 工具类型和复杂 stories 的最佳实践。https://#/HF8pvlqIqQ