Question about dash/extract-meta.js - adds extra set of quotes to string literal union types?

Hi there Dash community,

I’m posting to ask some questions about a specific build step for generating Dash components from a custom TypeScript component. I’m very comfortable with using Dash/Python, but this is my first attempt at creating my own custom Dash components. I’m also new to React and TypeScript, for context.

To get right to the issue I’m running into: I’m trying to declare a component prop with a literal string union type, and while the build:js and build:backends steps are completing without any errors (and generating usable Dash components), the generatedmetadata.json and proptypes.js are adding extra sets of quotation marks to the string literals.

I’ve based my overall project on the dash-typescript-component-template repo, though I’ve made a few edits to some of the example files (detail on that below).

Here’s an example TestComponent showing what I’m trying to make (note this is intentionally a trivial component):

/* File: /src/ts/components/TestComponent.tsx */
import React, {ReactNode} from "react";

import { DashComponentProps } from "props";

type SomeUnionOfStrings = "string1" | "string2" | "string3";

type Props = {
    children: ReactNode;
    foo?: SomeUnionOfStrings;
    bar?: "string4" | "string5";
} & DashComponentProps;

const TestComponent = (props: Props) => {
    const { 
        children, 
        foo, 
        bar 
    } = props;

    return (
        <div>
            {children}
            {foo && <div>Foo: {foo}</div>}
            {bar && <div>Bar: {bar}</div>}
        </div>
    );
};

TestComponent.defaultProps = {
    foo: "string1",
    bar: "string4",
};

export default TestComponent;

Here is what is ending up in the generated metadata.json and proptypes.js files:

{
  "src/ts/components/TestComponent.tsx": {
    "displayName": "TestComponent",
    "description": "",
    "props": {
      "children": {
        "description": "",
        "required": true,
        "type": { "name": "node", "raw": "ReactNode" }
      },
      "foo": {
        "description": "",
        "required": false,
        "defaultValue": { "value": "'string1'", "computed": false },
        "type": {
          "name": "enum",
          "value": [
            { "value": "'string1'", "computed": false },
            { "value": "'string2'", "computed": false },
            { "value": "'string3'", "computed": false }
          ],
          "raw": "SomeUnionOfStrings"
        }
      },
      "bar": {
        "description": "",
        "required": false,
        "defaultValue": { "value": "'string4'", "computed": false },
        "type": {
          "name": "enum",
          "value": [
            { "value": "'string4'", "computed": false },
            { "value": "'string5'", "computed": false }
          ],
          "raw": "\"string4\" | \"string5\""
        }
      },
      "id": {
        "description": "Unique ID to identify this component in Dash callbacks.",
        "required": false,
        "type": { "name": "string", "raw": "string" }
      },
      "setProps": {
        "description": "Update props to trigger callbacks.",
        "required": true,
        "type": {
          "name": "func",
          "raw": "(props: Record<string, any>) => void"
        }
      }
    },
    "isContext": false
  }
}

// AUTOGENERATED FILE - DO NOT EDIT

var pt = window.PropTypes;
var pk = window['test_component'];

pk.TestComponent.propTypes = {children:pt.node,
 foo:pt.oneOf(["'string1'", "'string2'", "'string3'"]),
 bar:pt.oneOf(["'string4'", "'string5'"]),
 id:pt.string,
 setProps:pt.any};

The string literal unions are being parsed by dash/extract-meta.json as enum type (which may be fine?), and the literal string values are being doubly quoted like ”’string1’” .

When I go to try adding this component to an example Dash page (e.g.: TestComponent(children=["Test Component"], foo="string1", bar="string4") ), I get an error message that string1 isn’t valid but ‘string1' is a permitted value:

Invalid argument `foo` passed into TestComponent.
Expected one of ["'string1'","'string2'","'string3'"].

Failed component prop type: Invalid component prop `bar` of value `string4` supplied to `function(e){var t=e.children,n=e.foo,r=e.bar;return o.default.createElement("div",null,t,n&&o.default.createElement("div",null,"Foo: ",n),r&&o.default.createElement("div",null,"Bar: ",r))}`
Value provided: "string1" 

And then in case it is helpful, here are the contents of related files:

/* File: /src/ts/index.ts */
import TestComponent from "./components/TestComponent";

export {
    TestComponent
};
/* File: /package.json */
{
  "name": "test_component",
  "version": "0.0.0",
  "description": "...",
  "main": "index.ts",
  "scripts": {
    "build:js::dev": "webpack --mode development",
    "build:js": "webpack",
    "build:backends": "dash-generate-components ./src/ts/components test_component -p package-info.json --ignore \\.test\\.",
    "build": "npm run build:js && npm run build:backends",
    "watch": "npm run build:js::dev -- --watch"
  },
  "devDependencies": {
    "@types/react": "^17.0.39",
    "css-loader": "^6.7.1",
    "npm-run-all": "^4.1.5",
    "ramda": "^0.28.0",
    "react": "^18.3.1",        // revised from boilerplate project
    "react-docgen": "^5.4.3",  // revised from boilerplate project
    "react-dom": "^18.3.1",    // revised from boilerplate project
    "style-loader": "^3.3.1",
    "svg-inline-loader": "^0.8.2",   // added, not in boilerplate project
    "ts-loader": "^9.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
  },
  "peerDependencies": {
    "react": "^18.3.1",     // revised from boilerplate project
    "react-dom": "^18.3.1"  // revised from boilerplate project
  },
  "author": "...",
  "license": "..."
}
# File: /requirements.txt
dash[dev]>=3.1
wheel
build
/* File: /webpack.config.js */
const path = require('path');

const packagejson = require('./package.json');

const dashLibraryName = packagejson.name.replace(/-/g, '_');

module.exports = function (env, argv) {
    const mode = (argv && argv.mode) || 'production';
    const entry = [path.join(__dirname, 'src/ts/index.ts')];
    const output = {
        path: path.join(__dirname, dashLibraryName),
        filename: `${dashLibraryName}.js`,
        library: dashLibraryName,
        libraryTarget: 'umd',
    }

    const externals = {
        react: {
            commonjs: 'react',
            commonjs2: 'react',
            amd: 'react',
            umd: 'react',
            root: 'React',
        },
        'react-dom': {
            commonjs: 'react-dom',
            commonjs2: 'react-dom',
            amd: 'react-dom',
            umd: 'react-dom',
            root: 'ReactDOM',
        },
    };

    return {
        output,
        devtool: 'source-map',
        mode,
        entry,
        target: 'web',
        externals,
        resolve: {
            extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
        },
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    use: 'ts-loader',
                    exclude: /node_modules/,
                },
                {
                    test: /\.css$/,
                    use: [
                        {
                            loader: 'style-loader',
                            options: {
                                insert: function insertAtTop(element) {
                                    var parent = document.querySelector("head");
                                    var lastInsertedElement =
                                        window._lastElementInsertedByStyleLoader;

                                    if (!lastInsertedElement) {
                                        parent.insertBefore(element, parent.firstChild);
                                    } else if (lastInsertedElement.nextSibling) {
                                        parent.insertBefore(element, lastInsertedElement.nextSibling);
                                    } else {
                                        parent.appendChild(element);
                                    }

                                    window._lastElementInsertedByStyleLoader = element;
                                },
                            },
                        },
                        {
                            loader: 'css-loader',
                        },
                    ],
                },
                {
                    test: /\.svg$/,
                    loader: 'svg-inline-loader'
                },
            ]
        }
    }
}

I’m using Node 16.14.0, TypeScript 4.95, and coding in VS Code on macOS. My tsconfig.json file is identical to the boilerplate repo, also.


I’ve tried stepping through the code for dash-generate-components and the supporting scripts, and as best as I can tell, it seems like my string literal values are being fed into coerceValue() inside dash/extract-meta.js, where the extra set of single quotes is being added. I can trace that back to getPropType() and see that maybe isUnionLiteral() is catching my string literal union type props, but I haven’t figured out a way around this.

Am I on the right track? Any ideas what might be causing the extra quotes to be added to my string literals in metadata.json?

Any help is appreciated, and thanks in advance!

Hi @justinminnion

That’s a great write-up. It would be good to open an issue in the dash GitHub

The dash-mantine-components library also uses TypeScript and I’ve also run into lots of issues with generating the prop types.

Until this is fixed, you can disable the runtime type checking by excluding it in __init__.py. Make sure this is not included:

_js_dist.append(dict(
    dev_package_path="proptypes.js",
    dev_only=True,
    namespace="test_component"
))

You will see the following message during the build, but it can be disregarded until the type checking works better in Dash. Here’s what the warning message looks like in the DMC build:

One more tip - defaultProps is not compatible with Dash 3, which uses React 18, and you will see a warning message in the console. To fix this, set the defaults in the function attributes. For example:

const TestComponent = ({
   children, 
   foo = "string1", 
   bar = "string4" 
}: Props) => {
    

Then delete this part:

TestComponent.defaultProps = {
    foo: "string1",
    bar: "string4",
};

Thanks very much for this helpful and informative reply, Ann Marie!

I’ve marked your post as the solution because it provides a functional work-around given the current type checking code, so again thank you for your time and insights.

I’ll work on submitting an issue on GitHub, too. If time allows I’ll try to step through extract-meta.js, too (even if only for my own learning).