
用 TypeScript 编写 Story
了解如何为你的 story 添加类型,使其更易于编码且更健壮

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

使用 TypeScript 还可以使你的代码更健壮。在编写 story 时,你正在复制组件在你的应用程序中的使用方式。通过类型检查,你可以在编码时捕获错误和边缘情况。
Storybook 具有内置的 TypeScript 支持,因此你可以零配置入门。让我们深入了解 story 类型化的具体细节。
使用 Meta 和 Story 实用程序类型对 story 进行类型化
在编写 story 时,有两个方面有助于类型化。第一个是 组件元数据,它描述和配置组件及其 story。在 CSF 文件中,这是默认导出。第二个是 story 本身。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 函数或 加载器 都将进行更完整的类型化。
// 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 函数添加类型
通常会 提取模板函数 以在多个 story 之间共享。例如,上面代码段中的 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 story 正在克隆 Template 函数。通过启用 strictBindCallApply
TypeScript 选项,你的 story 可以自动继承 Template 的类型。换句话说,你无需在每个 story 上重新声明类型。你可以在你的 tsconfig 中启用此选项。
为自定义 args 添加类型
有时,story 需要定义组件属性中未包含的 args。例如,List 组件可以包含 ListItem 组件,你可能希望为渲染的 ListItems 数量提供一个 控件。
对于这种情况,你可以使用 交叉类型 来扩展你作为 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 组件通常不导出其属性的类型。因此,React 的 Storybook 公开了 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,这是一种更紧凑和可组合的 story 编写方式。这是第一个代码段,重写为 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 编写 story 可以更轻松地开发更健壮的组件。你获得了类型安全和错误检查、自动完成建议等等。
Storybook 提供零配置的 TypeScript 支持。你可以进一步 自定义 此设置以更好地满足你的需求。此外,Storybook 文档中的所有代码段都以 JavaScript 和 TypeScript 两种风格提供。

如果你想继续讨论,请加入 Storybook Discord 中的 #typescript
频道。在那里见!
用 @typescript 编写 story 可以提高你的工作效率。
— Storybook (@storybookjs) August 4, 2022
你的 IDE 会在你编码时捕获错误,并且你获得更好的自动完成功能!
查看我们的新指南,了解如何为 story 添加类型。它涵盖了 Storybook 实用程序类型和复杂 story 的最佳实践。https://127.0.0.1/HF8pvlqIqQ