Search code examples
reactjstypescripttypescript-genericsstorybooktsx

Storybook: How to annotate generic args/props types


Context & Code

We extended our button like so:

export type ButtonProps<P = Record<string, unknown>> = P & {
  children: ReactNode | string
  onClick?: () => void
  className?: string
  disabled?: boolean
  secondary?: boolean
}

const Button = <P extends Record<string, unknown>>({
  children,
  onClick,
  className,
  disabled = false,
  secondary = false,
}: ButtonProps<P>) => {
  ...
}

This overloading means we can implement new buttons with additional props, without messing with the original button, à la:

const FooterTwoButtons = ({
  secondaryOnClick,
  secondaryIconType,
  icon,
  className,
  ...props
}: ButtonProps<TwoButtonsProps>) => {
  return (
    <div className={clsx('grid grid-cols-5', className)}>
      <Button onClick={secondaryOnClick} secondary>
        {(secondaryIconType !== undefined && (
          <Icon type={secondaryIconType} />
        )) ||
          (icon !== undefined && icon)}
      </Button>
      <FooterButton {...props}>{props.children}</FooterButton>
    </div>
  )
}

It's super nice as it allows more ephemeral button components to be extended from our base type. Everything compiles and works as expected, except for storybook...

Problem

I cannot seem to find any questions/issues of a modern storybook component allowing for generic props.

ExtendedButtonProps story:

export const ButtonExtendedProps: ComponentStory<typeof Button> = ({
  secondaryIconType,
  children,
  ...props
}: ButtonProps<ExtendedProps>) => {
  return (
    <div>
      <Button secondaryIconType={secondaryIconType} {...props}>
        {children}
      </Button>
    </div>
  )
}
ButtonExtendedProps.args = {
  ...Default.args,
  secondaryIconType: 'CircledX',
}

Here is the tsc compiler error, which appears on ButtonExtendedProps in the above code:

module ButtonExtendedProps
const ButtonExtendedProps: ComponentStory<(<P extends Record<string, unknown>>({ children, onClick, className, disabled, secondary, }: ButtonProps<P>) => JSX.Element)>
Type '{ ({ secondaryIconType, children, ...props }: ButtonProps<ExtendedProps>): JSX.Element; args: Partial<Record<string, unknown> & { children: ReactNode; onClick?: (() => void) | undefined; className?: string | undefined; disabled?: boolean | undefined; secondary?: boolean | undefined; }> | undefined; }' is not assignable to type 'ComponentStory<(<P extends Record<string, unknown>>({ children, onClick, className, disabled, secondary, }: ButtonProps<P>) => Element)>'.
  Type '{ ({ secondaryIconType, children, ...props }: ButtonProps<ExtendedProps>): JSX.Element; args: Partial<Record<string, unknown> & { children: ReactNode; onClick?: (() => void) | undefined; className?: string | undefined; disabled?: boolean | undefined; secondary?: boolean | undefined; }> | undefined; }' is not assignable to type 'ArgsStoryFn<ReactFramework, Record<string, unknown> & { children: ReactNode; onClick?: (() => void) | undefined; className?: string | undefined; disabled?: boolean | undefined; secondary?: boolean | undefined; }>'.
    Types of parameters '__0' and 'args' are incompatible.
      Type 'Record<string, unknown> & { children: ReactNode; onClick?: (() => void) | undefined; className?: string | undefined; disabled?: boolean | undefined; secondary?: boolean | undefined; }' is not assignable to type 'ButtonProps<ExtendedProps>'.
        Property 'secondaryIconType' is missing in type 'Record<string, unknown> & { children: ReactNode; onClick?: (() => void) | undefined; className?: string | undefined; disabled?: boolean | undefined; secondary?: boolean | undefined; }' but required in type 'ExtendedProps'.ts(2322)

Can anyone spot what I've missed here? Is there a page in the storybook docs which provides examples of this? I looked around quite a bit today, but not sure which keywords to search. Any tips are much appreciated. Cheers.


Solution

  • As it would happen, ComponentStory<typeof Button> === Story<ButtonProps>

    So the resulting story signature looked like:

    export const ButtonExtendedProps: Story<ButtonProps<ExtendedProps>> = ({
      altIconType,
      altClassName,
      altAriaLabel,
      children,
      clean,
      ...props
    }: ButtonProps<ExtendedProps>) => {