Search code examples
reactjstypescripttype-inferencestorybook

Using Storybook and TypeScript, how can you infer a control for a prop that references an interface defined in a different component?


My team is working on a component library using Storybook.

The Storybook controls for each component are generally inferred using TypeScript. For example, we might use a union for one of our props, like this:

export interface ButtonProps {
  color: 'red' | 'green' | 'blue';
}

Then in our story, we don't need to define the control for that argument. It will automatically be inferred and Storybook will define the control as a <select> field with each color value as an option (the union is inferred).

Storybook color control as select

This works well. However, let's say we add a new component that contains the <Button> component. Perhaps we want to be able to change the color of that Button in this new component. We'll use a color prop for the <NewComponent> and reference the same type.

import { ButtonProps } from 'component-library';

export interface NewComponentProps {
  color: ButtonProps['color'];
}

This allows us to pass in any color that is in the ButtonProps['color'] union to the <NewComponent>, just like we would do with the <Button> itself.

However, Storybook does not seem to be able to infer the options for NewComponentProps['color']. Instead of rendering the <select> field with all of the available options in the union, Storybook renders a plain text <input>, which isn't what we want.

Storybook color control as input

It seems to be inferring the value as a string and not the union. This means you could enter anything you want, which isn't the case.

How can we solve this? Will we need to explicitly define the control for props that reference other component types?


Solution

  • It turns out it was not an issue of referencing another component's prop types, but instead, it had to do with the return type of the component.

    Previously I was not using an explicit return type, but after adding FC<Props> as the return type for the component, it worked.

    BEFORE

    const NewComponent = ({ color }) => {
      ...
    }
    

    AFTER

    import { FC } from 'react';
    
    const NewComponent: FC<NewComponentProps> = ({ color }) => {
      ...
    }
    

    Now the color prop is inferred correctly as the defined union.

    enter image description here