[RFC]: Typesafe CSF factories #30112
Replies: 8 comments 11 replies
-
Love this, have definitely been feeling the pain points listed here!
// AllOfMy.stories.ts
import preview from '#.storybook/preview';
import { Button } from './Button';
import { Tooltip } from './Tooltip';
const meta = preview.meta({ component: Button });
export const Primary = meta.story({
args: {
primary: true,
label: 'Button',
},
});
const tooltipMeta = preview.meta({ component: Tooltip });
export const Tooltip = tooltipMeta.story({
args: {
primary: true,
label: 'Tooltip',
},
}); This might be out of scope but one other thing regarding typing that could be improved is that parameters are not typed. Maybe there is a way to allow for type augmentation of parameters here in a way that we could get autocomplete when writing e.g. const tooltipMeta = preview.meta({ component: Tooltip });
export const Tooltip = tooltipMeta.story({
args: {
primary: true,
label: "Tooltip",
},
parameters: {
docs: {
description: {
story: "Hover to see the tooltip.",
},
story: {
height: "500px",
inline: false,
},
},
},
}); Maybe this is already possible? |
Beta Was this translation helpful? Give feedback.
-
I like the general direction of this proposal. The only point I'm not so sure is the proposed syntax. Coming from a vue-background, the following would feel the most natural: import { defineMeta, defineStory } from '@storybook/...'
import { Button } from './Button';
defineMeta({ component: Button });
defineStory({
name: 'Primary'
args: {
primary: true,
label: 'Button',
},
}); Perhaps with auto-import of the |
Beta Was this translation helpful? Give feedback.
-
I'm a big fan of this. The lack of intellisense and type-checking for things like The ability to extend a config would also be a big boon to our monorepo at work where we have a consolidated storybook, but there are often decorators or parameters that should be applied to all stories in a particular package. The current requirement for |
Beta Was this translation helpful? Give feedback.
-
I like the proposal. What I wonder is how to handle types for decorators visible for the story args? This has been one of the pain points with current model. How I have handled it currently is following: export type DecoratorArray = readonly Decorator[];
// Utility type to extract props from decorator
export type DecoratorProps<T extends Decorator> =
T extends Decorator<infer P> ? P : never;
/**
* @description Convert a union type to an intersection type
* @example
* type A = { a: 1 };
* type B = { b: 2 };
* type C = { c: 3 };
* type ABC = UnionToIntersection<A | B | C>
* // ABC = { a: 1 } & { b: 2 } & { c: 3 }
*/
export type UnionToIntersection<Union> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Union extends any ? (argument: Union) => void : never) extends (
argument: infer Intersection,
) => void
? Intersection
: never;
export type UnionOfDecorators<T extends DecoratorArray> = T[number];
/**
* Given an array of decorator functions, returns the props of the union of those decorators.
*
* Example:
* type Props = DecoratorProps<UnionOfDecorators<[(props: { a: string }) => void, (props: { b: number }) => void]>>;
* // type Props = { a: string } | { b: number }
*/
export type DecoratorTypes<T extends DecoratorArray> = DecoratorProps<
UnionOfDecorators<T>
>;
/**
* Returns the intersection of the types that the decorators in the array
* implement.
*/
export type IntersectionOfDecoratorTypes<T extends DecoratorArray> =
UnionToIntersection<DecoratorTypes<T>>;
/**
* Props type for a Storybook decorator that accepts a component and additional decorator types.
* @typeParam ComponentType - The type of the component being decorated.
* @typeParam DecoratorTypes - An array of decorator types to be applied to the component.
*/
export type ArgProps<
ComponentType extends // eslint-disable-next-line @typescript-eslint/no-explicit-any
React.JSXElementConstructor<any> | keyof JSX.IntrinsicElements,
DecoratorTypes extends DecoratorArray,
> = React.ComponentProps<ComponentType> &
IntersectionOfDecoratorTypes<DecoratorTypes>; These types are used like this: const decorators = [formProviderDecorator] as const;
type Args = ArgProps<typeof AutoCompleteField, typeof decorators>;
const meta = {
component: AutoCompleteField,
title: 'Form Components/AutoCompleteField',
decorators: [...decorators],
args: {
showDevTool: true,
},
} satisfies Meta<Args>; I would really love to have something build in instead. |
Beta Was this translation helpful? Give feedback.
-
This looks really promising :) |
Beta Was this translation helpful? Give feedback.
-
Looks really nice. coming from a vue background, it would be nice if we can type the argtypes purly using typescript. Like the defineProps api in vue
|
Beta Was this translation helpful? Give feedback.
-
Love this! This mirrors the solution I came up with when documenting a generic Select component in a type-safe way, I decided to use a factory function: const meta = {
title: 'forms/<SelectAsync>',
component: SelectAsync,
args: {
children: (items) =>
items.map((itm) => (
<SelectItem key={itm.value} value={itm.value}>
{itm.label}
</SelectItem>
)),
},
parameters: {
api: { disable: false },
},
} satisfies Meta<typeof SelectAsync>;
export default meta;
type Story<T extends string, TQueryData, TQueryError extends Error, TQueryKey extends QueryKey> = StoryObj<
typeof SelectAsync<T, TQueryData, TQueryError, TQueryKey>
>;
/** {@link makeStory} is a helper function like {@link queryOptions} to allow us to typecheck our story correctly. */
function makeStory<T extends string, TQueryData, TQueryError extends Error, TQueryKey extends QueryKey>(
story: Story<T, TQueryData, TQueryError, TQueryKey>
) {
return story;
}
/**
* Loads items immediately
*/
export const Default = makeStory({
args: {
label: 'Option',
getItemsQueryOptions: makeGetItemsQueryOptions(0),
},
}); Excited to see this in the actual storybook project! |
Beta Was this translation helpful? Give feedback.
-
The ergonomics of this seem really nice. Feels like a meaningful step toward being able to think of a story as essentially just a renderable component that can be reused from tests (though I'm sure it's far more complex under the hood). However, my experiences trying to get it to work have all ended in dead ends. Not sure the docs are out-of-date, if I'm doing something wrong, or if the particular framework I'm using (NextJS) hasn't been updated for compatibility. I tried to make the docs' suggested changes demo projects based on both Storybook 8.6.12 and 9.0.0-beta.10, and in both cases I end up with story exports that don't seem to match my expectations based on the docs. Expectation: Actual: Happy to share more about my setup if that would help, but more just curious what the progress is on this reaching a fully supported release! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
RFC: Typesafe CSF factories
Storybook's current syntax lacks the modern, developer-friendly features that competing tools offer, such as autocomplete and discoverability. Developers need to remember Storybook-specific jargon, which creates a high cognitive load. Other tools in the ecosystem have standardized the factory pattern (e.g.
defineConfig
), which offers intuitive autocomplete and suggestions that make the experience seamless. Without these features, users struggle to configure addons correctly and may overlook valuable features Storybook offers.Try it out today!
You can try out this new experimental CSF factory syntax in your React projects (more renderers will be supported later) today.
npx storybook migrate csf-2-to-3 --glob="**/*.stories.tsx" --parser=tsx
.storybook/vitest.setup.ts
file. ThesetProjectAnnotations
call is done automatically with CSF factories.Manual upgrade instructions
Update your preview config file
The ability for an addon to provide annotation types (
parameters
,globals
, etc.) is new and not all addons support it yet.If an addon provides annotations (i.e. it distributes a
./preview
export), it can be imported in two ways:For official Storybook addons, you import the default export:
import addonName from '@storybook/addon-name'
For community addons, you should import the entire module and access the addon from there:
import * as addonName from 'community-addon-name/preview'
Update your story files
Story files have been updated for improved usability. With the new format:
preview.meta
function and does not have to be exported as a default exportmeta.story
functionNote that importing or manually applying any type to the meta or stories is no longer necessary. Thanks to the factory function pattern, the types are now inferred automatically.
Reusing story properties
If you want to reuse story properties from another story, you can use the
extend
API:Important
All sections marked with ✅ are implemented in this canary.
At this time, only React projects are supported.
Introducing CSF factories ✅
Writing a type-safe CSF3 story typically involves significant boilerplate. Here's an example of a standard CSF3 story for a
Button
component:To streamline this process, we can introduce factories that require no extra type annotations for type safety. Here's how the
Button
story would look using factories:This approach reduces the need for explicit type declarations and manual repetitive code, minimizing the chances of errors. It makes the story definitions more concise and easier to understand.
The preview file ✅
The preview file is central to configuring Storybook's behavior and addons. Here's a
.storybook/preview.ts
file using the proposeddefinePreview
factory:Importantly, by specifying addons here, their types will be available throughout your project, enabling autocompletion and type checking.
You will import the result of this function,
preview
, in your story files to define the component meta.The preview configuration will be automatically updated to reference the necessary addons when installing an addon via
npx storybook add <addon-name>
. In the future we will do a runtime check when runningstorybook dev
, making sure that the addons defined inmain.ts
andpreview.ts
always stay in sync.The
preview.meta
factory ✅The
meta
function on thepreview
object is used to define the metadata for your stories. It accepts an object containing thecomponent
,title
,parameters
, and other story properties.Note: While you can use relative imports for the preview file, we recommend adopting this standard-based absolute import convention, popularized by Kent C. Dodds. Learn more about this convention. We provide the option to automatically set this up when migrating with
npx storybook automigrate csf-factories
.The
meta.story
factory ✅Finally, the
story
factory on themeta
object defines the stories. This function accepts an object containing thename
,args
,parameters
and other story properties.Factory for CSF1-style stories ✅
It is still popular to write stories as a component when using React:
We will keep supporting this pattern with factories:
Resolving the documentation burden ✅
Currently, there are 3 syntaxes that users can write configuration in (TS 4.9+, TS4.8- and pure JS):
Supporting three different syntaxes (TypeScript 4.9+, TypeScript 4.8-, pure Javascript) increases documentation complexity and user confusion. With factories, we can have the same syntax for all users. And it achieves the same or better type safety, but with a single clean syntax that provides great autocompletion:
Make a default export optional ✅
Previous we needed the meta to be exported. For CSF factories this is not necessary anymore:
The meta runtime information is automatically merged with every story, so we don’t need it for that reason.
We also extract some data at compile time for the sidebar (like the name, title of the story).
The runtime information with factories will be automatically merged. And the compile time information, can also be extracted by looking at the
meta
call in the AST tree.Stories are portable by default ✅
This also means that stories are completely portable by default. Every story will be fully prepared with all the preview and meta annotations, and you don’t need to call setProjectAnnotations in your test setup (e.g. for the vitest integration).
Here's an example of how you can reuse a story in a test file by rendering its component:
Extending a story ✅
You will be able to create a new story from an existing story:
Here, the same inheritance is used as when you go from
meta
to astory
. Args and parameters are deep-merged, decorators are added, and render/play function is overridden.Infer type information from argTypes/globalTypes 🚧
In some cases, the props of the component are less interesting than the child that you can put inside of it. For this, you might want to use custom args that are unrelated to the props.
Getting this correct with TypeScript in CSF3 is quite a challenge at the moment. But with factories we can support inferred patterns like below:
Another case, where we can infer the type from runtime information is
globals
:Multiple preview configs 🚧
Stories for page components might need completely different defaults than story for design system components. Being able to write multiple story configs solves this:
Non-component args (Future proposal) 🚧
At some point we want to support non-component args prefixed with a
$
. Normally, all args of the story are automatically applied as props of the component. We would like to introduce “$-prefixed-args” (aka non-component args) as a way to get all the power of controls and args for other purposes (aside from component props).In this way, you can use mock data in a fully declarative way and control this data in the control panel:
Non-component args are not a part of this proposal. Another proposal for that will come and we also consider making all parameters controllable in the controls addon, as that would fulfil the same purpose of non-component args.
Beta Was this translation helpful? Give feedback.
All reactions