diff --git a/code/renderers/react/src/__test__/CSF3.typetest.tsx b/code/renderers/react/src/__test__/CSF3.typetest.tsx new file mode 100644 index 000000000000..ee1a7aab8f9b --- /dev/null +++ b/code/renderers/react/src/__test__/CSF3.typetest.tsx @@ -0,0 +1,109 @@ +import React, { ComponentType, KeyboardEventHandler, ReactElement, ReactNode } from 'react'; +import { Meta, Story } from '../public-types'; + +interface ButtonProps { + label: string; + disabled: boolean; +} + +declare const Button: (props: ButtonProps) => JSX.Element; + +/** + * Mimicking the satisfies operator. + */ +function satisfies() { + return (x: T) => x; +} + +// ✅ valid +export const meta1 = satisfies>()({ + component: Button, + args: { label: 'good', disabled: false }, +}); + +export const Basic1: Story = {}; + +// // ✅ valid +export const meta2 = satisfies>()({ + component: Button, + args: { label: 'good' }, +}); + +export const Basic2: Story = { + args: { disabled: false }, +}; + +// ❌ invalid +const meta3 = satisfies>()({ + component: Button, +}); + +export const Basic3: Story = { + // @ts-expect-error disabled not provided + args: { label: 'good' }, +}; + +// ❌ invalid +const meta4 = satisfies>()({ + component: Button, + args: { label: 'good' }, +}); + +// @ts-expect-error disabled not provided +export const Basic4: Story = {}; + +// ❌ invalid +const meta5 = satisfies>()({ + component: Button, +}); + +export const Basic5: Story = { + // @ts-expect-error disabled not provided + args: { label: 'good' }, +}; + +interface ButtonProps2 { + label: string; + disabled: boolean; + onClick(): void; + onKeyDown: KeyboardEventHandler; + onLoading: (s: string) => JSX.Element; + submitAction(): void; +} + +declare const Button2: (props: ButtonProps2) => JSX.Element; + +// ✅ valid +const meta6 = satisfies>()({ + component: Button2, + args: { label: 'good' }, +}); + +export const Basic6: Story = { + // all functions are optional, except onLoading + args: { disabled: false, onLoading: () =>
Loading...
}, +}; + +type ThemeData = 'light' | 'dark'; +declare const Theme: (props: { theme: ThemeData; children?: ReactNode }) => JSX.Element; +type Props = ButtonProps & { theme: ThemeData }; + +export const meta7 = satisfies>()({ + component: Button, + args: { label: 'good', disabled: false }, + render: (args, { component }) => { + // component is not null as it is provided in meta + // TODO: Might be nice if we can infer that. + // eslint-disable-next-line + const Component = component!; + return ( + + + + ); + }, +}); + +export const Basic: Story = { + args: { theme: 'light' }, +}; diff --git a/code/renderers/react/src/public-types.ts b/code/renderers/react/src/public-types.ts index bf26a71a7787..dd7abbc53819 100644 --- a/code/renderers/react/src/public-types.ts +++ b/code/renderers/react/src/public-types.ts @@ -1,5 +1,5 @@ import { AnnotatedStoryFn, Args, ComponentAnnotations, StoryAnnotations } from '@storybook/csf'; -import { ComponentProps, JSXElementConstructor } from 'react'; +import { ComponentProps, ComponentType, JSXElementConstructor } from 'react'; import { ReactFramework } from './types'; type JSXElement = keyof JSX.IntrinsicElements | JSXElementConstructor; @@ -9,7 +9,12 @@ type JSXElement = keyof JSX.IntrinsicElements | JSXElementConstructor; * * @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export) */ -export type Meta = ComponentAnnotations; +export type Meta< + CmpOrArgs = Args, + StoryArgs = CmpOrArgs extends ComponentType ? CmpArgs : CmpOrArgs +> = CmpOrArgs extends ComponentType + ? ComponentAnnotations, StoryArgs> + : ComponentAnnotations, StoryArgs>; /** * Story function that represents a CSFv2 component example. @@ -54,14 +59,23 @@ export type ComponentStoryFn = StoryFn>; */ export type ComponentStoryObj = StoryObj>; -/** - /** * Story function that represents a CSFv3 component example. * * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) */ -export type Story = StoryObj; +export type Story< + MetaOrArgs = Args, + StoryArgs = MetaOrArgs extends { component: ComponentType } ? CmpArgs : MetaOrArgs +> = MetaOrArgs extends { + component: ComponentType; + args?: infer D; +} + ? StoryAnnotations, StoryArgs> & + ({} extends MakeOptional> + ? unknown + : { args: MakeOptional> }) + : StoryAnnotations; /** * For the common case where a (CSFv3) story is a simple component that receives args as props: @@ -72,3 +86,9 @@ export type Story = StoryObj; * } * ``` */ export type ComponentStory = ComponentStoryObj; + +type ActionArgs = { + [P in keyof Args as ((...args: any[]) => void) extends Args[P] ? P : never]: Args[P]; +}; + +type MakeOptional = Omit & Partial>>; diff --git a/code/renderers/react/src/types.ts b/code/renderers/react/src/types.ts index 51ab7f4d9fbc..3d4dc6f7657e 100644 --- a/code/renderers/react/src/types.ts +++ b/code/renderers/react/src/types.ts @@ -3,8 +3,8 @@ import type { ComponentType, ReactElement } from 'react'; export type { RenderContext } from '@storybook/store'; export type { StoryContext } from '@storybook/csf'; -export type ReactFramework = { - component: ComponentType; +export type ReactFramework = { + component: ComponentType; storyResult: StoryFnReactReturnType; }; diff --git a/code/renderers/vue3/src/public-types.ts b/code/renderers/vue3/src/public-types.ts index 7d1185e6ca6f..00df2ce187af 100644 --- a/code/renderers/vue3/src/public-types.ts +++ b/code/renderers/vue3/src/public-types.ts @@ -1,13 +1,28 @@ +import type { ComponentOptionsBase } from 'vue'; import type { + AnnotatedStoryFn, Args, ComponentAnnotations, StoryAnnotations, - AnnotatedStoryFn, } from '@storybook/csf'; import { VueFramework } from './types'; export type { Args, ArgTypes, Parameters, StoryContext } from '@storybook/csf'; +// TODO +export type PropsOf = T extends ComponentOptionsBase< + infer Props, + any, + any, + any, + any, + any, + any, + any +> + ? Props + : never; + /** * Metadata to configure the stories for a component. *