Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to add docs for TypeScript extended types? #13637

Open
Vadorequest opened this issue Jan 14, 2021 · 18 comments
Open

How to add docs for TypeScript extended types? #13637

Vadorequest opened this issue Jan 14, 2021 · 18 comments

Comments

@Vadorequest
Copy link

I use the following type:

export type Props = {
  /**
   * Style applied to the code block. [See `codeBlockStyle` type](https://github.com/rajinwonderland/react-code-blocks/blob/31e391b30a1f2835aaad4275f542329239761182/packages/react-code-blocks/src/components/CodeBlock.tsx#L19)
   */
  codeBlockStyle?: CSSStyles;

  /**
   * Style applied to the code block. [See `codeContainerStyle` type](https://github.com/rajinwonderland/react-code-blocks/blob/31e391b30a1f2835aaad4275f542329239761182/packages/react-code-blocks/src/components/CodeBlock.tsx#L19)
   */
  codeContainerStyle?: CSSStyles;

  /**
   * Text displayed as source code.
   */
  text: string;
} & Partial<CodeBlockProps>;

The extended type is

import React, { PureComponent } from 'react';
import { applyTheme } from '../utils/themeBuilder';
import { Theme } from '../types';
import Code from './Code';

export interface CodeBlockProps {
  /** The code to be formatted */
  text: string;
  /** The language in which the code is written. [See LANGUAGES.md](https://github.com/rajinwonderland/react-code-blocks/blob/master/LANGUAGES.md) */
  language: string;
  /** Indicates whether or not to show line numbers */
  showLineNumbers?: boolean;
  /** A custom theme to be applied, implements the `CodeBlockTheme` interface. You can also pass pass a precomposed theme into here. For available themes. [See THEMES.md](https://github.com/rajinwonderland/react-code-blocks/blob/master/THEMES.md) */
  theme?: Theme;
  /** The element or custom react component to use in place of the default `span` tag */
  lineNumberContainerStyle?: {};
  /** The style object to apply to the `CodeBlock` text directly i.e `fontSize` and such */

  codeBlockStyle?: {};
  /** The style object that accesses the style parameter on the `codeTagProps` property on the `Code` component */
  codeContainerStyle?: {};

  /** The style object that will be combined with the top level style on the pre tag, styles here will overwrite earlier styles. */
  customStyle?: {};

  /**
   * Lines to highlight comma delimited.
   * Example uses:

   * - To highlight one line `highlight="3"`
   * - To highlight a group of lines `highlight="1-5"`
   * - To highlight multiple groups `highlight="1-5,7,10,15-20"`
   */
  highlight?: string;
}

const LANGUAGE_FALLBACK = 'text';

export default class CodeBlock extends PureComponent<CodeBlockProps, {}> {
  _isMounted = false;

  static displayName = 'CodeBlock';

  static defaultProps = {
    showLineNumbers: true,
    language: LANGUAGE_FALLBACK,
    theme: {},
    highlight: '',
    lineNumberContainerStyle: {},
    customStyle: {},
    codeBlockStyle: {},
  };

  componentDidMount() {
    this._isMounted = true;
  }
  componentWillUnmount() {
    this._isMounted = false;
  }
  handleCopy = (event: any) => {
    /**
     * We don't want to copy the markup after highlighting, but rather the preformatted text in the selection
     */
    const data = event.nativeEvent.clipboardData;
    if (data) {
      event.preventDefault();
      const selection = window.getSelection();
      if (selection === null) {
        return;
      }
      const selectedText = selection.toString();
      const document = `<!doctype html><html><head></head><body><pre>${selectedText}</pre></body></html>`;
      data.clearData();
      data.setData('text/html', document);
      data.setData('text/plain', selectedText);
    }
  };

  render() {
    const {
      lineNumberContainerStyle,
      codeBlockStyle,
      codeContainerStyle,
    } = applyTheme(this.props.theme);

    const props = {
      language: this.props.language || LANGUAGE_FALLBACK,
      codeStyle: {
        ...codeBlockStyle,
        ...this.props?.codeBlockStyle,
      },
      customStyle: this.props?.customStyle,
      showLineNumbers: this.props.showLineNumbers,
      codeTagProps: {
        style: {
          ...codeContainerStyle,
          ...this.props?.codeContainerStyle,
        },
      },
      lineNumberContainerStyle: {
        ...lineNumberContainerStyle,
        ...this.props?.lineNumberContainerStyle,
      },
      text: this.props.text.toString(),
      highlight: this.props.highlight,
    };

    return <Code {...props} />;
  }
}

But Storybook renders only the props defined in Props, not those in CodeBlockProps.
image

Is it the expected behaviour? I'm still new to storybook, I had seen something about TS types extending somewhere but can't find it again.

@dfmartin
Copy link

Is this what you are looking for? https://storybook.js.org/docs/react/configure/typescript

@Vadorequest
Copy link
Author

Vadorequest commented Jan 28, 2021

Using

  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      compilerOptions: {
        allowSyntheticDefaultImports: false,
        esModuleInterop: false,
      },
    },
  },

With the following story:

import {
  Meta,
  Story,
} from '@storybook/react/types-6-0';
import React from 'react';
import Code, { Props } from '@/common/components/dataDisplay/Code';

export default {
  title: 'Next Right Now/Data display/Code',
  component: Code,
  argTypes: {},
} as Meta;

const defaultText = `
  import { css } from '@emotion/react';
  import React from 'react';
  import AnimatedLoader from '../svg/AnimatedLoader';

  export type Props = {}

  const Loader: React.FunctionComponent<Props> = (props): JSX.Element => {
    return (
      <div
        css={css\`
          justify-content: center;
          text-align: center;
          margin-left: auto;
          margin-right: auto;
        \`}
      >
        <AnimatedLoader />
      </div>
    );
  };

  export default Loader;
`;

const Template: Story<Props> = (props) => {
  return (
    <Code
      text={defaultText}
      {...props}
    />
  );
};

export const DynamicExample: Story<Props> = Template.bind({});
DynamicExample.args = {
  text: defaultText,
};

Generates the doc:

image

I lose all props that aren't defined in the story.


Using the same component with

  typescript: {
    check: false,
    checkOptions: {},
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },

Generates:
image

Basically, using a custom typescript config in main.js doesn't improve anything. Props included by CodeBlockProps aren't listed. (is it because they are Partial?)

@texttechne
Copy link

This is default behaviour of react-docgen-typescript: typings of third party libs are excluded by default.
Here is the proper configuration.

@texttechne
Copy link

Turning this configuration on, however, might also not be what you want: You really do see ALL props.
For example, extending interfaces of components which extend any HTMLElement, will end in you scrolling through a long list of all possible HTML properties (global stuff, aria-*, events: clipboard, drag, click, tap, ...). There should be countless other examples...

@Vadorequest
Copy link
Author

Yeah, I had tried that and it was way too noisy to be usable.

@texttechne
Copy link

The only possible route then is: turn the config on to see ALL types and build your own types
=> facilitate TypeScript constructs like Pick, Omit, Exclude, Extract, ...

Possibly, a lot of effort.

@texttechne
Copy link

What would be really fantastic: if react-docgen-typescript would create seperate documentation tabs per extended interface / type

@shilman
Copy link
Member

shilman commented May 29, 2021

@texttechne There's an open issue for that here. I've been meaning to fix it for months but other things have taken priority. #7943

@texttechne
Copy link

@shilman Thanks for the linked context! Cool, that this feature is already planned. 👍

If Storybook provides this feature of making huge type hierarchies browsable, then this issue is solved en-passant. This would actually be a huge and really cool feature.

Apart from that, there's nothing that Storybook can do: The rest has to be solved in TypeScript-land. In the end it's a problem with the typings themselves that we are facing. The typings of HTMLElements (provided by React, e.g. ButtonHTMLAttributes, HTMLAttributes) are just they way they are: really exhaustive, not modular and probably quite imperfect.

@texttechne
Copy link

texttechne commented May 29, 2021

Here is what we can do with TypeScript: Build our own interface without the need to redefine known props.

Example

My component: Autocomplete
Library component: MultiInput (extends HTMLAttributes<HTMLElement> which is huge in regards to props)

// Magic TS helper function
type ExcludedTypes<T, U> = {
  [K in Exclude<keyof T, keyof U>]: T[K];
};

// exclude all props belonging to standard HTML elements
type ReducedMultiInputProps = ExcludedTypes<MultiInputPropTypes, HTMLAttributes<HTMLElement>>;
// pick only those props which we do care about
type SharedProps = Pick<MultiInputPropTypes, "style" | "className" | "id" | "onChange" | "placeholder">;
// union of previous prop sets and exclude props of the library interface, that we don't want
type WantedMultiInputProps = SharedProps | Omit<ReducedMultiInputProps, "tokens">;

// finally: union of the wanted props with our own props
export type AutoCompleteProps =
  | WantedMultiInputProps
  | {
      values?: AutoCompleteItemsProps;
      suggestions?: Array<string>;
    };

The Magis TS helper function

Result

The docs now only show the following props:

  • all props from library interface (MultiInputProp)
    • caveat: redefined props (props which occur in MultiInputProp & also in HTMLELement) have been excluded and need to be reintroduced; "style", "className", ...
    • feature: omit the props you don't want from the library
  • selected props from HTMLElement
    • we have to pick from this mass, thererby creating a positive list => utitily types could be helpful
  • own props / props of own component

Conclusion

  • We don't have to reinvent the wheel, i.e. repeat any prop definition
    • we take as much over from the original interface as wanted
    • even picking /omitting is only the repetition of the prop name, but not the definition itself
  • We have fine-granular control over the exposed API of our own components
    • => finally a correct documentation (not leaving anything out)

@Snivio
Copy link

Snivio commented Mar 27, 2023

I want to see my props plus all the mantine props in the storybook but seems like only props defined by me are shown in the storybook

My type files


import {
  InputVariant,
  TextInputProps as MantineTextInputProps,
} from '@mantine/core';

export type MantineTextInputPropsType = MantineTextInputProps &
  React.RefAttributes<HTMLInputElement>;

export interface TextInputProps extends MantineTextInputPropsType {
  title: string;
  description?: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
  disabled?: boolean;
  variant?: InputVariant;
  isRequired?: boolean;
  maxLength?: number;
  value?: string;
}

My storyBook file


import { Meta, Story } from '@storybook/react';
import { ChangeEvent, useState } from 'react';
import { TextInput as StoryBookTextInput } from './';
import { TextInputProps } from './TextInput';

export default {
  title: 'core/atoms/textInputs/TextInput',
  component: StoryBookTextInput,
} as Meta;

const Template: Story<TextInputProps> = ({ ...args }) => {
  const [value, setValue] = useState('');
  const onChange = (e: ChangeEvent<HTMLInputElement>) =>
    setValue(e.currentTarget.value);
  return <StoryBookTextInput value={value} onChange={onChange} {...args} />;
};

export const TextInput = Template.bind({});

TextInput.args = {
  title: 'Title',
  description: 'This is the description',
  disabled: false,
  gainsightId: 'text-input',
  placeholder: 'Placeholder text',
};

The StoryBook output just displays

  title: string;
  description?: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
  disabled?: boolean;
  variant?: InputVariant;
  isRequired?: boolean;
  maxLength?: number;
  value?: string;

and not the other mantine props

i tried configuring both vite plugin and storybook main with
'react-docgen-typescript'

Tried Plugins: @joshwooding/vite-plugin-react-docgen-typescript, react-docgen-typescript-vite-plugin

tried both react doc-gen configs

// .storybook/main.js

module.exports = {
  typescript: {
    check: false,
    checkOptions: {},
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },
};

and

// .storybook/main.js

module.exports = {
  stories: [],
  addons: [],
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      compilerOptions: {
        allowSyntheticDefaultImports: false,
        esModuleInterop: false,
      },
    }
  }
};

Even tried

const { mergeConfig } = require('vite');
const viteTsConfigPaths = require('vite-tsconfig-paths').default;
const reactDocgenTypescript = require('@joshwooding/vite-plugin-react-docgen-typescript');

module.exports = {
  core: { builder: '@storybook/builder-vite' },
  stories: ['../../**/*.stories.mdx', '../../**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials'],
  plugins: [reactDocgenTypescript()],
  typescript: {
    check: false,
    checkOptions: {},
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) =>
        prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
    },
  },
  async viteFinal(config, { configType }) {
    return mergeConfig(config, {
      plugins: [
        viteTsConfigPaths({
          root: '../../../',
        }),
      ],
    });
  },
};

@shilman shilman added this to the 8.1 annotations server milestone Jun 8, 2023
@gerardparareda
Copy link

gerardparareda commented Sep 26, 2023

I have the same issue as @Snivio reported. With MaterialUI I can choose using Pick which controls to show, as explained in https://storybook.js.org/blog/material-ui-in-storybook/ with the use of

import { Typography as MuiTypography, TypographyProps as MuiTypographyProps } from '@mui/material';
export type TypographyBaseProps = Pick<MuiTypographyProps, 'children' | 'variant'>;

but then I can't set any other prop that's not picked, for example noWrap or className.
Here's the Component for context:

export const Typography = ({
  children,
  ...rest
}: TypographyBaseProps) => {
  return (
    <MuiTypography {...rest}>{children}</MuiTypography>
  );
};

Now, using this component in other places anything that's passed on via ...rest becomes not resolvable and therefore useless.

import { Typography } from "./Typography.component";

...
  return (<Typography variant="h6" noWrap className="toolbar-app-name">);
                                   ^^^^^^^ this breaks

@texttechne
Copy link

Hi @gerardparareda,

that's a different issue. You're not extending some base props and adding new props to it. The issue there is that the base props from which the own interface extends are not shown.

By using Pick you create an own type which only consists of the picked elements. You're restricting the amount of props.
So it actually works like designed...

But what are you trying to achieve by picking? Do you rather want to Omit some props?

@gerardparareda
Copy link

gerardparareda commented Sep 27, 2023

Thanks @texttechne, I understand what you mean.

What I want is for the controls to show only my picked types and keep using the rest of the types when using the component directly. For example using noWrap or className in the .tsx but not having it show in the controls.

Does this go against the Storybook workflow? Does everything I use need to be picked? I don't need to show all the controls MUI components offer.

@texttechne
Copy link

texttechne commented Sep 27, 2023

Hey @gerardparareda,

your use case is really a different one: Actually you want to exploit the described bug 😄.
It seems you would be able to pull this off by a) picking those props you want to be documented and b) extend the base props which then would not be shown but still usable.

However, I am unable to formulate this properly in TS:

// here we're extending two times => now, nothing should be visible
export interface MyTypoProps extends Pick<BaseTypoProps, "children" | "variant">, BaseTypoProps {}

// semantically the same as above, but now you would see every prop again, which makes the picking part pretty useless
export type MyTypoProps = Pick<BaseTypoProps, "children" | "variant"> & BaseTypoProps

I'm not claiming it's impossible, but I just don't see right now how to do that...

Alternatively, here are my 2 cents: I would recommend to use Omit (see Utility Types) which is the opposite of Pick: Show all available props that matter, but leave those out which you are not using. So you would name those props which you don't want to keep the documentation clean. However, all props that are usable would be advertised, which is a good thing... well, in my opinion...

@gerardparareda
Copy link

The "problem" with both solutions is that the same interface used in the component constructor is used in the storybook controls. So, whatever I do, either I have all the controls and props of a MUI component or I'm going to miss a lot of functionality in exchange for visibility.

@alex-braun
Copy link

alex-braun commented Oct 24, 2023

By omitting the more verbose typings, I'm seeing good results at getting extended types from external libraries with:

//  .storybook/main.ts

  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      shouldRemoveUndefinedFromOptional: true,
      // NOTE: this default cannot be changed
      savePropValueAsString: true,
      // Storybook default is to never include props from node_modules:
      // propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
      propFilter: (prop: any) => {
        if (prop.parent && /node_modules/.test(prop.parent.fileName)) {
          // console.debug(prop.parent) // uncomment to print all props within node_modules
          return !(
            prop.parent.name === 'AnchorHTMLAttributes' ||
            prop.parent.name === 'AriaAttributes' ||
            prop.parent.name === 'ButtonHTMLAttributes' ||
            prop.parent.name === 'DOMAttributes' ||
            prop.parent.name === 'FormHTMLAttributes' ||
            prop.parent.name === 'HTMLAttributes' ||
            prop.parent.name === 'ImgHTMLAttributes' ||
            prop.parent.name === 'InputHTMLAttributes'
          );
        }
        return true;
      },
    },
  },

I might play with it more, but our controls and mdx docs are more comprehensive now.

@tronsix
Copy link

tronsix commented Jun 3, 2024

@Snivio I was able to get Mantine props to show within storybook using the following config. This is the same config I use for Material-UI. I just changed the prop filter.

    // .storybook/main.ts

    typescript: {
    check: false,
    /**
     * For improved speed use react-docgen instead of react-docgen-typescript
     * Use react-docgen-typescript for verbose documentation of mantine components
     */
    reactDocgen: "react-docgen-typescript", // use react-docgen instead of react-docgen-typescript to improve speed
    reactDocgenTypescriptOptions: {
      // Speeds up Storybook build time
      compilerOptions: {
        allowSyntheticDefaultImports: false,
        esModuleInterop: false,
      },
      // Makes union prop types like variant and size appear as select controls
      shouldExtractLiteralValuesFromEnum: true,
      // Makes string and boolean types that can be undefined appear as inputs and switches
      shouldRemoveUndefinedFromOptional: true,
      // Filter out third-party props from node_modules except @mantine packages
      propFilter: (prop) =>
        prop.parent
          ? !/node_modules\/(?!@mantine)/.test(prop.parent.fileName)
          : true,
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants