From 656fbfac507176d2dd02ce0d989d1e66dc883b2c Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Thu, 3 Aug 2023 10:18:48 -0600 Subject: [PATCH 1/3] Add Writing Stories in TypeScript docs guide - Use same snippets for all "custom args" examples - Add/update/remove snippets --- docs/essentials/controls.md | 26 +-- docs/snippets/angular/page-story-slots.ts.mdx | 29 ++-- ...able-story-fully-customize-controls.ts.mdx | 39 ----- docs/snippets/angular/typed-csf-file.ts.mdx | 22 +++ .../snippets/common/typed-csf-file.ts-4-9.mdx | 23 +++ docs/snippets/common/typed-csf-file.ts.mdx | 23 +++ docs/snippets/react/page-story-slots.js.mdx | 15 +- .../react/page-story-slots.ts-4-9.mdx | 25 ++- docs/snippets/react/page-story-slots.ts.mdx | 23 ++- ...able-story-fully-customize-controls.js.mdx | 34 ---- ...-story-fully-customize-controls.ts-4-9.mdx | 39 ----- ...able-story-fully-customize-controls.ts.mdx | 39 ----- docs/snippets/solid/page-story-slots.js.mdx | 15 +- .../solid/page-story-slots.ts-4-9.mdx | 26 ++- docs/snippets/solid/page-story-slots.ts.mdx | 24 ++- ...able-story-fully-customize-controls.js.mdx | 34 ---- ...-story-fully-customize-controls.ts-4-9.mdx | 39 ----- ...able-story-fully-customize-controls.ts.mdx | 39 ----- docs/snippets/vue/page-story-slots.2.js.mdx | 13 +- .../vue/page-story-slots.2.ts-4-9.mdx | 24 ++- docs/snippets/vue/page-story-slots.2.ts.mdx | 24 ++- docs/snippets/vue/page-story-slots.3.js.mdx | 13 +- .../vue/page-story-slots.3.ts-4-9.mdx | 24 ++- docs/snippets/vue/page-story-slots.3.ts.mdx | 24 ++- ...le-story-fully-customize-controls.2.js.mdx | 34 ---- ...tory-fully-customize-controls.2.ts-4-9.mdx | 39 ----- ...le-story-fully-customize-controls.2.ts.mdx | 39 ----- ...le-story-fully-customize-controls.3.js.mdx | 36 ----- ...tory-fully-customize-controls.3.ts-4-9.mdx | 41 ----- ...le-story-fully-customize-controls.3.ts.mdx | 41 ----- .../web-components/page-story-slots.js.mdx | 15 +- .../web-components/page-story-slots.ts.mdx | 17 +- ...able-story-fully-customize-controls.js.mdx | 37 ----- ...able-story-fully-customize-controls.ts.mdx | 42 ----- .../web-components/typed-csf-file.ts.mdx | 21 +++ docs/toc.js | 5 + docs/writing-stories/typescript.md | 149 ++++++++++++++++++ 37 files changed, 392 insertions(+), 760 deletions(-) delete mode 100644 docs/snippets/angular/table-story-fully-customize-controls.ts.mdx create mode 100644 docs/snippets/angular/typed-csf-file.ts.mdx create mode 100644 docs/snippets/common/typed-csf-file.ts-4-9.mdx create mode 100644 docs/snippets/common/typed-csf-file.ts.mdx delete mode 100644 docs/snippets/react/table-story-fully-customize-controls.js.mdx delete mode 100644 docs/snippets/react/table-story-fully-customize-controls.ts-4-9.mdx delete mode 100644 docs/snippets/react/table-story-fully-customize-controls.ts.mdx delete mode 100644 docs/snippets/solid/table-story-fully-customize-controls.js.mdx delete mode 100644 docs/snippets/solid/table-story-fully-customize-controls.ts-4-9.mdx delete mode 100644 docs/snippets/solid/table-story-fully-customize-controls.ts.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.2.js.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.2.ts-4-9.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.2.ts.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.3.js.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.3.ts-4-9.mdx delete mode 100644 docs/snippets/vue/table-story-fully-customize-controls.3.ts.mdx delete mode 100644 docs/snippets/web-components/table-story-fully-customize-controls.js.mdx delete mode 100644 docs/snippets/web-components/table-story-fully-customize-controls.ts.mdx create mode 100644 docs/snippets/web-components/typed-csf-file.ts.mdx create mode 100644 docs/writing-stories/typescript.md diff --git a/docs/essentials/controls.md b/docs/essentials/controls.md index 5266c0760f02..cc0f937ec97b 100644 --- a/docs/essentials/controls.md +++ b/docs/essentials/controls.md @@ -123,26 +123,26 @@ If you haven't used the CLI to setup the configuration, or if you want to define ## Fully custom args -Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that aren’t part of the component. +Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that aren’t part of the component. For example, here's how you could use a `footer` arg to populate a child component: diff --git a/docs/snippets/angular/page-story-slots.ts.mdx b/docs/snippets/angular/page-story-slots.ts.mdx index 2b79a6caa3e3..9612456a8bef 100644 --- a/docs/snippets/angular/page-story-slots.ts.mdx +++ b/docs/snippets/angular/page-story-slots.ts.mdx @@ -5,26 +5,27 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { Page } from './page.component'; -const meta: Meta = { - component: Page, -}; - -export default meta; -type Story = StoryObj; +// TODO: Is this correct? +// If not, is it expected that an Angular component exports its props type? +// What if it's an interface instead of a type (which seems to be the convention)? +// Or, how do you extract an Angular component's props type? +type PagePropsAndCustomArgs = Page & { footer?: string }; -/* - *πŸ‘‡ Render functions are a framework specific feature to allow you control on how the component renders. - * See https://storybook.js.org/docs/angular/api/csf - * to learn how to use render functions. - */ -export const CustomFooter: Story = { - render: (args) => ({ +const meta: Meta = { + component: Page, + render: ({ footer, ...args }) => ({ props: args, template: ` - ${args.footer} + ${footer} `, }), +}; +export default meta; + +type Story = StoryObj; + +export const CustomFooter: Story = { args: { footer: 'Built with Storybook', }, diff --git a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx b/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx deleted file mode 100644 index 387ca9506c93..000000000000 --- a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx +++ /dev/null @@ -1,39 +0,0 @@ -```ts -// Table.stories.ts - -import type { Meta, StoryObj } from '@storybook/angular'; - -import { Table } from './Table.component'; - -const meta: Meta = { - component: Table, -}; - -export default meta; -type Story = StoryObj
; - -export const Numeric: Story = { - render: (args) => ({ - props: args, - template: ` -
- - - - - -
- {{data[i][j]}} -
- `, - }), - args: { - data: [ - [1, 2, 3], - [4, 5, 6], - ], - //πŸ‘‡ The remaining args get passed to the `Table` component - size: 'large', - }, -}; -``` diff --git a/docs/snippets/angular/typed-csf-file.ts.mdx b/docs/snippets/angular/typed-csf-file.ts.mdx new file mode 100644 index 000000000000..6ac8b473a93b --- /dev/null +++ b/docs/snippets/angular/typed-csf-file.ts.mdx @@ -0,0 +1,22 @@ +```ts +// Button.stories.ts + +import type { Meta, StoryObj } from '@storybook/angular'; + +import { Button } from './button.component'; + +const meta: Meta + + +``` + +The same setup works with Svelte stories files too, providing both type safety and autocompletion. + + + + From 84f89999917fb09977972ca75fe0b164c6333b88 Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Thu, 10 Aug 2023 15:46:07 -0500 Subject: [PATCH 2/3] Address TODOs --- docs/snippets/angular/page-story-slots.ts.mdx | 4 ---- docs/snippets/solid/page-story-slots.ts-4-9.mdx | 4 ++-- docs/snippets/solid/page-story-slots.ts.mdx | 4 ++-- docs/snippets/vue/page-story-slots.2.ts-4-9.mdx | 5 +++-- docs/snippets/vue/page-story-slots.2.ts.mdx | 5 +++-- docs/snippets/vue/page-story-slots.3.ts-4-9.mdx | 5 +++-- docs/snippets/vue/page-story-slots.3.ts.mdx | 5 +++-- docs/snippets/web-components/page-story-slots.ts.mdx | 7 +++---- docs/snippets/web-components/typed-csf-file.ts.mdx | 2 -- 9 files changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/snippets/angular/page-story-slots.ts.mdx b/docs/snippets/angular/page-story-slots.ts.mdx index 9612456a8bef..63ac7f3309cc 100644 --- a/docs/snippets/angular/page-story-slots.ts.mdx +++ b/docs/snippets/angular/page-story-slots.ts.mdx @@ -5,10 +5,6 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { Page } from './page.component'; -// TODO: Is this correct? -// If not, is it expected that an Angular component exports its props type? -// What if it's an interface instead of a type (which seems to be the convention)? -// Or, how do you extract an Angular component's props type? type PagePropsAndCustomArgs = Page & { footer?: string }; const meta: Meta = { diff --git a/docs/snippets/solid/page-story-slots.ts-4-9.mdx b/docs/snippets/solid/page-story-slots.ts-4-9.mdx index ecae504fb858..83431768d59e 100644 --- a/docs/snippets/solid/page-story-slots.ts-4-9.mdx +++ b/docs/snippets/solid/page-story-slots.ts-4-9.mdx @@ -1,12 +1,12 @@ ```tsx // Page.stories.ts|tsx +import type { ComponentProps } from 'solid-js'; import type { Meta, StoryObj } from 'storybook-solidjs'; import { Page } from './Page'; -// TODO: How do you extract a Solid component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta = { component: Page, diff --git a/docs/snippets/solid/page-story-slots.ts.mdx b/docs/snippets/solid/page-story-slots.ts.mdx index e7a718c92a65..c372bf6e38d7 100644 --- a/docs/snippets/solid/page-story-slots.ts.mdx +++ b/docs/snippets/solid/page-story-slots.ts.mdx @@ -1,12 +1,12 @@ ```tsx // Page.stories.ts|tsx +import type { ComponentProps } from 'solid-js'; import type { Meta, StoryObj } from 'storybook-solidjs'; import { Page } from './Page'; -// TODO: How do you extract a Solid component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta: Meta = { component: Page, diff --git a/docs/snippets/vue/page-story-slots.2.ts-4-9.mdx b/docs/snippets/vue/page-story-slots.2.ts-4-9.mdx index 6b1b4716c0c5..2d6c35078af6 100644 --- a/docs/snippets/vue/page-story-slots.2.ts-4-9.mdx +++ b/docs/snippets/vue/page-story-slots.2.ts-4-9.mdx @@ -1,12 +1,13 @@ ```ts // Page.stories.ts +// https://www.npmjs.com/package/vue-component-type-helpers +import type { ComponentProps } from 'vue-component-type-helpers'; import type { Meta, StoryObj } from '@storybook/vue'; import Page from './Page.vue'; -// TODO: How do you extract a Vue2 component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta = { component: Page, diff --git a/docs/snippets/vue/page-story-slots.2.ts.mdx b/docs/snippets/vue/page-story-slots.2.ts.mdx index 6d419658802c..b43d05f9b85e 100644 --- a/docs/snippets/vue/page-story-slots.2.ts.mdx +++ b/docs/snippets/vue/page-story-slots.2.ts.mdx @@ -1,12 +1,13 @@ ```ts // Page.stories.ts +// https://www.npmjs.com/package/vue-component-type-helpers +import type { ComponentProps } from 'vue-component-type-helpers'; import type { Meta, StoryObj } from '@storybook/vue'; import Page from './Page.vue'; -// TODO: How do you extract a Vue2 component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta: Meta = { component: Page, diff --git a/docs/snippets/vue/page-story-slots.3.ts-4-9.mdx b/docs/snippets/vue/page-story-slots.3.ts-4-9.mdx index 26ffa616088f..afb00a498398 100644 --- a/docs/snippets/vue/page-story-slots.3.ts-4-9.mdx +++ b/docs/snippets/vue/page-story-slots.3.ts-4-9.mdx @@ -1,12 +1,13 @@ ```ts // Page.stories.ts +// https://www.npmjs.com/package/vue-component-type-helpers +import type { ComponentProps } from 'vue-component-type-helpers'; import type { Meta, StoryObj } from '@storybook/vue3'; import Page from './Page.vue'; -// TODO: How do you extract a Vue3 component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta = { component: Page, diff --git a/docs/snippets/vue/page-story-slots.3.ts.mdx b/docs/snippets/vue/page-story-slots.3.ts.mdx index b850a423e22c..fc7eb08b54fd 100644 --- a/docs/snippets/vue/page-story-slots.3.ts.mdx +++ b/docs/snippets/vue/page-story-slots.3.ts.mdx @@ -1,12 +1,13 @@ ```ts // Page.stories.ts +// https://www.npmjs.com/package/vue-component-type-helpers +import type { ComponentProps } from 'vue-component-type-helpers'; import type { Meta, StoryObj } from '@storybook/vue3'; import Page from './Page.vue'; -// TODO: How do you extract a Vue3 component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type PagePropsAndCustomArgs = ComponentProps & { footer?: string }; const meta: Meta = { component: Page, diff --git a/docs/snippets/web-components/page-story-slots.ts.mdx b/docs/snippets/web-components/page-story-slots.ts.mdx index 255014b3f7f6..aa4d7c7a09c9 100644 --- a/docs/snippets/web-components/page-story-slots.ts.mdx +++ b/docs/snippets/web-components/page-story-slots.ts.mdx @@ -5,10 +5,9 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; -// TODO: How do you extract a web component's props type? -// type PagePropsAndCustomArgs = Page & { footer?: string }; +type CustomArgs = { footer?: string }; -const meta: Meta = { +const meta: Meta = { title: 'Page', component: 'demo-page', render: ({ footer }) => html` @@ -19,7 +18,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const CustomFooter: Story = { args: { diff --git a/docs/snippets/web-components/typed-csf-file.ts.mdx b/docs/snippets/web-components/typed-csf-file.ts.mdx index e34f97ca1bc0..7814ea60fdda 100644 --- a/docs/snippets/web-components/typed-csf-file.ts.mdx +++ b/docs/snippets/web-components/typed-csf-file.ts.mdx @@ -3,8 +3,6 @@ import type { Meta, StoryObj } from '@storybook/web-components'; -// TODO: How do you extract a web component's props type? - const meta: Meta = { title: 'Button', component: 'demo-button', From dbde31bb5b7d8cfc7afc7af4b76fa7291f067923 Mon Sep 17 00:00:00 2001 From: Kyle Gach Date: Tue, 15 Aug 2023 12:07:14 -0600 Subject: [PATCH 3/3] Address comments - Simplify introduction - Fix links - Tweak "type parameter" heading & text - Fix typos - Simplify renderer-specific content structure --- docs/writing-stories/typescript.md | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/writing-stories/typescript.md b/docs/writing-stories/typescript.md index 077736e38f18..17d2c0b6eaca 100644 --- a/docs/writing-stories/typescript.md +++ b/docs/writing-stories/typescript.md @@ -2,15 +2,13 @@ title: 'Writing stories in TypeScript' --- -Writing your stories in [TypeScript](https://www.typescriptlang.org/) makes you more productive. You don't have to jump between files to look up component props. Your code editor will alert you about missing required props and even autocomplete prop values. Plus, Storybook infers those component types to auto-generate the [Controls](../api/doc-block-controls.md) table. - -Using TypeScript also makes your code more robust. When authoring stories, you're replicating how a component will be used within your app. With type checking, you can catch bugs and edge cases as you code. +Writing your stories in [TypeScript](https://www.typescriptlang.org/) makes you more productive. You don't have to jump between files to look up component props. Your code editor will alert you about missing required props and even autocomplete prop values, just like when using your components within your app. Plus, Storybook infers those component types to auto-generate the [Controls](../api/doc-block-controls.md) table. Storybook has built-in TypeScript support, so you can get started with zero configuration required. ## Typing stories with `Meta` and `StoryObj` -When writing stories, there are two aspects that are helpful to type. The first is the [component meta](./introduction.md#default-export), which describes and configures the component and its stories. In a [CSF file](..api.md/csf), this is the default export. The second is the [stories themselves](./introduction.md#defining-stories). +When writing stories, there are two aspects that are helpful to type. The first is the [component meta](./introduction.md#default-export), which describes and configures the component and its stories. In a [CSF file](../api/csf.md), this is the default export. The second is the [stories themselves](./introduction.md#defining-stories). Storybook provides utility types for each of these, named `Meta` and `StoryObj`. Here's an example CSF file using those types: @@ -26,9 +24,9 @@ Storybook provides utility types for each of these, named `Meta` and `StoryObj`. -### Optional type parameter +### Props type parameter -`Meta` and `StoryObj` types are both [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html#working-with-generic-type-variables), so you can provide them with a prop type parameter for the component type or the component's props type (e.g., the `typeof Button` portion of `Meta`). By doing so, TypeScript will prevent you from defining an invalid arg, and all [decorators](./decorators.md), [play functions](./play-function.md), or [loaders](./loaders.md) will type their function arguments. +`Meta` and `StoryObj` types are both [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html#working-with-generic-type-variables), so you can provide them with an optional prop type parameter for the component type or the component's props type (e.g., the `typeof Button` portion of `Meta`). By doing so, TypeScript will prevent you from defining an invalid arg, and all [decorators](./decorators.md), [play functions](./play-function.md), or [loaders](./loaders.md) will type their function arguments. The example above passes a component type. See [**Typing custom args**](#typing-custom-args) for an example of passing a props type. @@ -47,7 +45,7 @@ Both Angular and Web components utilize a class plus decorator approach. The dec 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 [this discussion](github.com/storybookjs/storybook/discussions/20988). +For more information, please refer to [this discussion](https://github.com/storybookjs/storybook/discussions/20988). @@ -57,7 +55,7 @@ For more information, please refer to [this discussion](github.com/storybookjs/s If you are using TypeScript 4.9+, you can take advantage of the new [`satisfies`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html) operator to get stricter type checking. Now you will receive type errors for missing required args, not just invalid ones. -Using `satisfies` to apply a story's type helps maintain type safety when sharing a `play` function across stories. Without it, TypeScript will throw an error that the `play` function may be undefined. The `satisfies` operator enables TypeScript to infer whether the play is defined or not. +Using `satisfies` to apply a story's type helps maintain type safety when sharing a `play` function across stories. Without it, TypeScript will throw an error that the `play` function may be undefined. The `satisfies` operator enables TypeScript to infer whether the play function is defined or not. Finally, use of `satisfies` allows you to pass `typeof meta` to the `StoryObj` generic. This informs TypeScript of the connection between the `meta` and `StoryObj` types, which allows it to infer the `args` type from the `meta` type. In other words, TypeScript will understand that args can be defined both at the story and meta level and won't throw an error when a required arg is defined at the meta level, but not at the story level. @@ -82,15 +80,9 @@ Sometimes stories need to define args that aren’t included in the component's - - -## 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 specific tips 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): @@ -121,9 +113,9 @@ This setup will add type support for `*.vue` imports to your `*.stories.ts` file -### Svelte +### Svelte specific tips -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](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode&ssr=false#overview). +Svelte 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](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode&ssr=false#overview). ```html