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

argTypes not generated correctly for Props of a TypeScript React Component with an Interface which extends another Type which uses ComponentProps and Pick #14798

Open
tschanz-pcv opened this issue May 4, 2021 · 6 comments

Comments

@tschanz-pcv
Copy link

Describe the bug
In a Create React App TypeScript project I have a FunctionComponent with the following prop interface:

type PickedAntButtonProps = Pick<
  ComponentProps<typeof AntButton>,
  'size' | 'disabled' | 'icon' | 'loading' | 'onClick'
>;

export interface DefaultButtonProps extends PickedAntButtonProps {
  /**
   * Set primary style
   */
  primary?: boolean;
  /**
   * Fit button to parent width
   */
  stretch?: boolean;
}

Storybook won't pick up these props correctly and only generate Controls & Doc Table entries for primary, stretch and for some reason onClick but the Control seems to be an Object definition (OnClick: {}).

I don't think that this is a react-docgen-typescript bug as I have run parse on this component directly and it seems to have generated the correct documentation.

To Reproduce

Prerequisites:

  • CRA 4.0.3
  • antd added to project & setup (add antd as a dependency and add the css import to your index.ts)
  • Storybook added to Project

Component with the issue:

import { ComponentProps, FunctionComponent } from 'react';
import { Button as AntButton } from 'antd';

type PickedAntButtonProps = Pick<
  ComponentProps<typeof AntButton>,
  'size' | 'disabled' | 'icon' | 'loading' | 'onClick'
>;

export interface DefaultButtonProps extends PickedAntButtonProps {
  /**
   * Set primary style
   */
  primary?: boolean;
  /**
   * Fit button to parent width
   */
  stretch?: boolean;
}

/**
 * Default button
 */
const DefaultButton: FunctionComponent<DefaultButtonProps> = ({
  children,
  primary = true,
  stretch = false,
  ...props
}) => {
  return (
    <AntButton block={stretch} type={primary ? 'primary' : undefined} {...props} data-testid="default-button">
      {children}
    </AntButton>
  );
};

export default DefaultButton;

The component returns and Ant Design button but only accepts a subset of the properties of that component and defines/renames some of them.

Running storybook will show the following props in Control and the Doc Table:
primary, stretch, onClick while onClick has a JSON control with onClick: {}.

I tried running react-docgen-typescript directly on the file using:

const docgen = require('react-docgen-typescript');

const options = {
  savePropValueAsString: true,
};

// Parse a file for docgen info
const docs = docgen.parse('./src/components/button/DefaultButton/DefaultButton.tsx', options);

console.log(JSON.stringify(docs, null, '\t'));

which results in the following output:

[
	{
		"tags": {},
		"description": "Default button",
		"displayName": "DefaultButton",
		"methods": [],
		"props": {
			"primary": {
				"defaultValue": {
					"value": "true"
				},
				"description": "Set primary style",
				"name": "primary",
				"parent": {
					"fileName": "src/components/button/DefaultButton/DefaultButton.tsx",
					"name": "DefaultButtonProps"
				},
				"declarations": [
					{
						"fileName": "src/components/button/DefaultButton/DefaultButton.tsx",
						"name": "DefaultButtonProps"
					}
				],
				"required": false,
				"type": {
					"name": "boolean"
				}
			},
			"stretch": {
				"defaultValue": {
					"value": "false"
				},
				"description": "Fit button to parent width",
				"name": "stretch",
				"parent": {
					"fileName": "src/components/button/DefaultButton/DefaultButton.tsx",
					"name": "DefaultButtonProps"
				},
				"declarations": [
					{
						"fileName": "src/components/button/DefaultButton/DefaultButton.tsx",
						"name": "DefaultButtonProps"
					}
				],
				"required": false,
				"type": {
					"name": "boolean"
				}
			},
			"onClick": {
				"defaultValue": null,
				"description": "",
				"name": "onClick",
				"declarations": [
					{
						"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
						"name": "TypeLiteral"
					},
					{
						"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
						"name": "TypeLiteral"
					}
				],
				"required": false,
				"type": {
					"name": "MouseEventHandler<HTMLElement>"
				}
			},
			"disabled": {
				"defaultValue": null,
				"description": "",
				"name": "disabled",
				"parent": {
					"fileName": "proj/node_modules/@types/react/index.d.ts",
					"name": "ButtonHTMLAttributes"
				},
				"declarations": [
					{
						"fileName": "proj/node_modules/@types/react/index.d.ts",
						"name": "ButtonHTMLAttributes"
					}
				],
				"required": false,
				"type": {
					"name": "boolean"
				}
			},
			"icon": {
				"defaultValue": null,
				"description": "",
				"name": "icon",
				"parent": {
					"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
					"name": "BaseButtonProps"
				},
				"declarations": [
					{
						"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
						"name": "BaseButtonProps"
					}
				],
				"required": false,
				"type": {
					"name": "ReactNode"
				}
			},
			"size": {
				"defaultValue": null,
				"description": "",
				"name": "size",
				"parent": {
					"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
					"name": "BaseButtonProps"
				},
				"declarations": [
					{
						"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
						"name": "BaseButtonProps"
					}
				],
				"required": false,
				"type": {
					"name": "SizeType"
				}
			},
			"loading": {
				"defaultValue": null,
				"description": "",
				"name": "loading",
				"parent": {
					"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
					"name": "BaseButtonProps"
				},
				"declarations": [
					{
						"fileName": "proj/node_modules/antd/lib/button/button.d.ts",
						"name": "BaseButtonProps"
					}
				],
				"required": false,
				"type": {
					"name": "boolean | { delay?: number; }"
				}
			}
		}
	}
]

which looks correct(-ish?) in my eyes, so seemingly all the props got detected and parsed correctly but Storybook does not seem to use them correctly.

Maybe this is just a configuration issue on my side or Storybook does not detect some things correctly. I've added my main.js further below. It could also be that the issue lies somewhere completely different and not the exotic typing.

System

  System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
  Binaries:
    Node: 14.16.1 - ~/.nvm/versions/node/v14.16.1/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v14.16.1/bin/yarn
    npm: 6.14.12 - ~/.nvm/versions/node/v14.16.1/bin/npm
  Browsers:
    Chrome: 90.0.4430.93
    Safari: 14.0.3
  npmPackages:
    @storybook/addon-actions: ^6.2.9 => 6.2.9
    @storybook/addon-essentials: ^6.2.9 => 6.2.9
    @storybook/addon-links: ^6.2.9 => 6.2.9
    @storybook/node-logger: ^6.2.9 => 6.2.9
    @storybook/preset-ant-design: 0.0.2 => 0.0.2
    @storybook/preset-create-react-app: ^3.1.7 => 3.1.7
    @storybook/react: ^6.2.9 => 6.2.9

Additional context

Storybook launch command: start-storybook -p 6006 -s public

.storybook/main.js:

I have various configurations to add import aliases, different ant themes based on flavor (it's a white labeled app), and ant design. I've included nearly everything so it should be easier to find the issue if it's simply a config issue.

const path = require('path');

/*
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["./src/components/*"]
    }
  }
}
*/
/*
and in tsconfig.json:
{
  "extends": "./tsconfig.paths.json",
  "compilerOptions": {
  ...
*/
const aliasPaths = require(path.join(__dirname, '/../tsconfig.paths.json'));

/*
const path = require('path');

const antThemes = {
  a: require(path.join(__dirname, '/a/theme.ant.json')),
  b: require(path.join(__dirname, '/b/theme.ant.json')),
  c: require(path.join(__dirname, '/c/theme.ant.json')),
};

module.exports = {
  antThemes,
};
*/
/*
{
  "@primary-color": "#ff3131",
  "@text-color": "rgba(0, 0, 0, 0.85)",
  "@btn-primary-color": "#ffffff"
}
*/
const antThemes = require(path.join(__dirname, '/../src/settings/themes.js')).antThemes;

// SNIP - Removed some logic to determine which flavor to build as this app uses different themes for whitelabling

/*
 * Auto Generate Path Aliases
 * Using the tsconfig.paths.json config file, generate WebPack path aliases
 */
const matchAliasName = /(.*)\/\*/;
const matchAliasPath = /\.\/(.*)\/\*/;
const webpackPathAliases = Object.entries(aliasPaths.compilerOptions.paths)
  .map((entry) => {
    const aliasNameMatch = entry[0].match(matchAliasName);
    const aliasPathMatch = entry[1][0].match(matchAliasPath);
    return [aliasNameMatch[1], aliasPathMatch[1]];
  })
  .reduce((acc, entry) => {
    acc[entry[0]] = path.join(__dirname, `/../${entry[1]}`);
    return acc;
  }, {});

/*
 * Main Storybook Config
 */
module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    {
      name: '@storybook/preset-create-react-app',
      options: {
        craOverrides: {
          fileLoaderExcludes: ['less'],
        },
      },
    },
    {
      name: '@storybook/preset-ant-design',
      options: {
        lessOptions: {
          modifyVars: antThemes[targetFlavor],
        },
      },
    },
  ],
  webpackFinal: (config) => {
    /*
     * Push REACT_APP_ environment variables into storybook webpack config.
     * Otherwise only STORYBOOK_ variables would be available.
     */
    const plugin = config.plugins.find((plugin) => plugin.definitions?.['process.env']);
    const reactAppEnv = Object.keys(process.env)
      .filter((name) => /^REACT_APP_/.test(name))
      .reduce((acc, name) => {
        acc[name] = `"${process.env[name]}"`;
        return acc;
      }, {});
    plugin.definitions['process.env'] = {
      ...plugin.definitions['process.env'],
      ...reactAppEnv,
    };

    return {
      ...config,
      resolve: {
        ...config.resolve,
        alias: {
          ...config.resolve.alias,
          ...webpackPathAliases,
        },
      },
    };
  },
};

preview.js is only slightly modified, 2 decorators for Suspense and i18n Providers and a globalTypes export for a Locale change button:

import { Suspense, useEffect } from 'react';
import { I18nextProvider } from 'react-i18next';
import '../src/index.css'; // Load global styles
import AppConfig from '../src/settings/config'; // Load app flavor theme
import i18n from '../src/services/i18n/i18next';

/*
 * Define Storybook globals
 */
export const globalTypes = {
  locale: {
    name: 'Locale',
    description: 'i18n locale',
    defaultValue: 'de',
    toolbar: {
      icon: 'globe',
      items: [
        { value: 'en', right: '🇺🇸', title: 'English' },
        { value: 'de', right: '🇩🇪', title: 'Deutsch' },
      ],
    },
  },
};

/*
 * react-i18next wrapper to change locale based on storybook global
 */
const I18nProviderWrapper = ({ children, i18n, locale }) => {
  useEffect(() => {
    i18n.changeLanguage(locale);
  }, [i18n, locale]);
  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};

export const parameters = {
  appFlavor: AppConfig.flavor,
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

/*
 * Story wrappers
 */
export const decorators = [
  (Story, { globals }) => (
    <I18nProviderWrapper locale={globals.locale} i18n={i18n}>
      <Story />
    </I18nProviderWrapper>
  ),
  (Story) => (
    <Suspense fallback="loading">
      <Story />
    </Suspense>
  ),
];
@tschanz-pcv
Copy link
Author

tschanz-pcv commented May 10, 2021

Looks like I might have spoken too soon and it is (at least partially) an issue of react-docgen-typescript as it seems to be related to: styleguidist/react-docgen-typescript#68 and styleguidist/react-docgen-typescript#56

In the example above one possible fix/workaround would be changing the interface to:

import { ButtonProps as AntButtonProps } from 'antd';

export interface DefaultButtonProps {
  size?: AntButtonProps['size'];
  disabled?: AntButtonProps['disabled'];
  icon?: AntButtonProps['icon'];
  loading?: AntButtonProps['loading'];
  onClick?: AntButtonProps['onClick'];
  className?: AntButtonProps['className'];
  /**
   * Set primary style
   */
  primary?: boolean;
  /**
   * Fit button to parent width
   */
  stretch?: boolean;
}

Which will generate the correct controls but is a lot more verbose.

Going the more direct way of Pick<AntButtonProps, 'size' | 'disabled' | 'icon' | 'loading' | 'onClick' | 'className'> doesn't work either.

@ghost
Copy link

ghost commented May 4, 2022

Running into this same issue. It would be nice if we didn't have to re-create types that simply link to existing types.

@yarinsa
Copy link

yarinsa commented Nov 15, 2022

Having the same issue here

@yarinsa
Copy link

yarinsa commented Nov 15, 2022

After some digging this worked for me:

// main.js
import { mergeConfig, UserConfig } from 'vite';
import { config as viteConfig } from './vite.config';
import * as reactDocgen from 'react-docgen-typescript';
export default {
  framework: '@storybook/react',
  stories: ['../stories/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    'storybook-addon-designs/preset',
    '@storybook/addon-docs',
  ],
  core: {
    builder: '@storybook/builder-vite',
    disableTelemetry: true,
  },
  features: {
    storyStoreV7: true,
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // By default react-doc-gen-typescript filters node_modules type, this includes antd types
      tsconfigPath: '../tsconfig.json',
      propFilter: (prop: any) => {
        const res = /antd/.test(prop.parent?.fileName) || !/node_modules/.test(prop.parent?.fileName);
        return prop.parent ? res : true;
      },
      // The following 2 options turns string types into string literals and allows
      shouldExtractLiteralValuesFromEnum: true,
      savePropValueAsString: true,
    },
  },
  async viteFinal(config: UserConfig, { configType }: { configType: string }) {
    console.log('configType', config);
    return mergeConfig(config, viteConfig({ command: 'serve', mode: configType.toLowerCase() }));
  },
};

@maxmorozoff
Copy link

maxmorozoff commented Jun 3, 2023

Had a similar issue.

Finally solved it with the following filterProp function:

propFilter: (prop) => {
  if (prop.name === 'children') {
    return true;
  }

  if (prop.parent) {
    return !/node_modules/.test(prop.parent?.fileName);
  }

  // Exclude native HTML props
  if (!prop.declarations) {
    return false;
  }

  return true;
},

@LeleDallas
Copy link

After some digging this worked for me:

// main.js
import { mergeConfig, UserConfig } from 'vite';
import { config as viteConfig } from './vite.config';
import * as reactDocgen from 'react-docgen-typescript';
export default {
  framework: '@storybook/react',
  stories: ['../stories/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    'storybook-addon-designs/preset',
    '@storybook/addon-docs',
  ],
  core: {
    builder: '@storybook/builder-vite',
    disableTelemetry: true,
  },
  features: {
    storyStoreV7: true,
  },
  typescript: {
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      // By default react-doc-gen-typescript filters node_modules type, this includes antd types
      tsconfigPath: '../tsconfig.json',
      propFilter: (prop: any) => {
        const res = /antd/.test(prop.parent?.fileName) || !/node_modules/.test(prop.parent?.fileName);
        return prop.parent ? res : true;
      },
      // The following 2 options turns string types into string literals and allows
      shouldExtractLiteralValuesFromEnum: true,
      savePropValueAsString: true,
    },
  },
  async viteFinal(config: UserConfig, { configType }: { configType: string }) {
    console.log('configType', config);
    return mergeConfig(config, viteConfig({ command: 'serve', mode: configType.toLowerCase() }));
  },
};

I tried it but it didn't work for all components for example the Button component but for Autocomplete works well

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

5 participants