diff --git a/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx b/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx index 7f1d97c40bcf..a5df412b1acc 100644 --- a/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx +++ b/code/renderers/react/src/__test-dts__/CSF3.test-d.tsx @@ -85,9 +85,8 @@ describe('Story args can be inferred', () => { const meta = satisfies>()({ component: Button, - args: { label: 'good', disabled: false }, + args: { disabled: false }, render: (args, { component }) => { - // TODO: Might be nice if we can infer that. // component is not null as it is provided in meta // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const Component = component!; @@ -99,28 +98,53 @@ describe('Story args can be inferred', () => { }, }); - const Basic: StoryObj = { args: { theme: 'light' } }; + const Basic: StoryObj = { args: { theme: 'light', label: 'good' } }; }); + const withDecorator: DecoratorFn<{ decoratorArg: number }> = (Story, { args }) => ( + <> + Decorator: {args.decoratorArg} + This Story allows optional TArgs, but the decorator only knows about the decoratorArg. It + should really allow optionally a Partial of TArgs. + + + ); + test('Correct args are inferred when type is widened for decorators', () => { type Props = ButtonProps & { decoratorArg: number }; - const withDecorator: DecoratorFn<{ decoratorArg: number }> = (Story, { args }) => ( - <> - Decorator: {args.decoratorArg} - This Story allows optional TArgs, but the decorator only knows about the decoratorArg. It - should really allow optionally a Partial of TArgs. - - - ); - const meta = satisfies>()({ component: Button, - args: { label: 'good', disabled: false }, + args: { disabled: false }, decorators: [withDecorator], }); // Yes, decorator arg is required - const Basic: StoryObj = { args: { decoratorArg: 0 } }; + const Basic: StoryObj = { args: { decoratorArg: 0, label: 'good' } }; + }); + + test('Correct args are inferred when type is widened for decorators and render functions', () => { + type Props = ButtonProps & { decoratorArg: number } & { theme: ThemeData }; + + const meta = satisfies>()({ + component: Button, + args: { disabled: false }, + decorators: [withDecorator], + loaders: [async ({ args }: { args: { label: string } }) => ({ data: args.label })], + render: (args, { component }) => { + // component is not null as it is provided in meta + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const Component = component!; + return ( + + + + ); + }, + }); + + const Basic: StoryObj = { + args: { decoratorArg: 0, label: 'good', theme: 'light' }, + }; }); }); diff --git a/code/renderers/react/src/public-types.ts b/code/renderers/react/src/public-types.ts index f8406ed20223..6d924f184163 100644 --- a/code/renderers/react/src/public-types.ts +++ b/code/renderers/react/src/public-types.ts @@ -1,7 +1,14 @@ -import { AnnotatedStoryFn, Args, ComponentAnnotations, StoryAnnotations } from '@storybook/csf'; +import type { + AnnotatedStoryFn, + Args, + ComponentAnnotations, + StoryAnnotations, + ArgsStoryFn, + DecoratorFunction, + LoaderFunction, +} from '@storybook/csf'; import { ComponentType, JSXElementConstructor } from 'react'; import { ReactFramework } from './types'; -import { ArgsStoryFn, DecoratorFunction } from '../../../../../csf/src'; type JSXElement = keyof JSX.IntrinsicElements | JSXElementConstructor; @@ -10,12 +17,9 @@ type JSXElement = keyof JSX.IntrinsicElements | JSXElementConstructor; * * @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export) */ -export type Meta< - CmpOrArgs = Args, - StoryArgs = CmpOrArgs extends ComponentType ? CmpArgs : CmpOrArgs -> = CmpOrArgs extends ComponentType - ? ComponentAnnotations, StoryArgs> - : ComponentAnnotations, StoryArgs>; +export type Meta = CmpOrArgs extends ComponentType + ? ComponentAnnotations + : ComponentAnnotations; /** * Story function that represents a CSFv2 component example. @@ -29,23 +33,29 @@ export type StoryFn = AnnotatedStoryFn; * * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) */ + export type StoryObj = MetaOrArgs extends { - render?: ArgsStoryFn; - decorators?: DecoratorFunction[]; + render?: ArgsStoryFn; + decorators?: DecoratorFunction[]; component?: ComponentType; - args?: infer D; + loaders?: LoaderFunction[]; + args?: infer DefaultArgs; } - ? (unknown extends StoryArgs ? CmpArgs : StoryArgs) extends infer Args - ? StoryAnnotations, Args> & StrictStoryArgs + ? CmpArgs & RArgs & DArgs & LArgs extends infer Args + ? StoryAnnotations< + ReactFramework, + Args, + SetOptional)> + > : never : StoryAnnotations; -type StrictStoryArgs = {} extends MakeOptional> - ? { args?: Partial } - : { args: MakeOptional> }; - type ActionArgs = { [P in keyof Args as ((...args: any[]) => void) extends Args[P] ? P : never]: Args[P]; }; -type MakeOptional = Omit & Partial>>; +type SetOptional = { + [P in keyof T as P extends K ? P : never]?: T[P]; +} & { + [P in keyof T as P extends K ? never : P]: T[P]; +}; diff --git a/code/renderers/react/src/types.ts b/code/renderers/react/src/types.ts index 3d4dc6f7657e..ba8eb40ec0b2 100644 --- a/code/renderers/react/src/types.ts +++ b/code/renderers/react/src/types.ts @@ -1,12 +1,13 @@ import type { ComponentType, ReactElement } from 'react'; +import type { AnyFramework } from '@storybook/csf'; export type { RenderContext } from '@storybook/store'; export type { StoryContext } from '@storybook/csf'; -export type ReactFramework = { - component: ComponentType; - storyResult: StoryFnReactReturnType; -}; +// export interface ReactFramework extends AnyFramework { +// component: ComponentType; +// storyResult: StoryFnReactReturnType; +// } export interface ShowErrorArgs { title: string; @@ -24,3 +25,23 @@ export interface IStorybookSection { kind: string; stories: IStorybookStory[]; } + +export type Framework = { + component: unknown; + T: unknown; // higher kinded type + storyResult: unknown; +}; + +export interface ReactFramework extends Framework { + component: ComponentType; // reference the higher kinded type + storyResult: StoryFnReactReturnType; +} + +// used like this: +interface ComponentAnnotations { + // specify the generic type with an intersection + component?: (TFramework & { T: TArgs })['component']; + + // falls back to unknown, if you don't intersect T + subcomponents?: Record; +}