Creating custom component with extendVariants(NextUI) fails with typescript extraction

722 views Asked by At

Im trying to create a components lib by extending some of NextUI's components.

I wanted to start simple by doing the classic button. But unfortuantly I got stuck... when I build the npm package I get this:

stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '../../../node_modules/.pnpm/registry.npmjs.org+@[email protected]_@[email protected][email protected][email protected]/node_modules/@nextui-org/system-rsc/dist/types'. This is likely not portable. A type annotation is necessary.      
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '../../../node_modules/.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@nextui-org/react-utils/dist/refs'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.
stories/components/Button/Button.tsx(55,7): error TS2742: The inferred type of 'MyButton' cannot be named without a reference to '.pnpm/registry.npmjs.org+@[email protected][email protected]/node_modules/@react-types/shared'. This is likely not portable. A type annotation is necessary.

With this error the npm package loses it's type safety when used.

Maybe I'm doing something wrong. First I did an exakt copy of the this, (also gives the error).

Then I added my own variants to the button to test, the end result is this:

import { extendVariants, Button as NextUiButton } from '@nextui-org/react';
import { ReactNode, Ref } from 'react';
import { type VariantProps } from 'tailwind-variants';

const MyButton = extendVariants(NextUiButton, {
    variants: {
        // <- modify/add variants
        color: {
            primary: 'bg-blue-500 text-black',
            secondary: 'bg-purple-500 text-black',
        },
        isDisabled: {
            true: 'bg-[#eaeaea] text-[#000] opacity-50 cursor-not-allowed',
        },
        size: {
            xs: 'px-unit-2 min-w-unit-12 h-unit-6 text-tiny gap-unit-1 rounded-small',
            md: 'px-unit-4 min-w-unit-20 h-unit-10 text-small gap-unit-2 rounded-small',
            xl: 'px-unit-8 min-w-unit-28 h-unit-14 text-large gap-unit-4 rounded-medium',
        },
    },
    defaultVariants: {
        // <- modify/add default variants
        color: 'primary',
        size: 'xl',
    },
    compoundVariants: [
        // <- modify/add compound variants
        {
            isDisabled: true,
            color: 'secondary',
            class: 'bg-[#84cc16]/80 opacity-100',
        },
    ],
});
interface MyButtonProps extends VariantProps<typeof MyButton> {
    ref?: Ref<HTMLButtonElement>;
    className?: string;
    children?: ReactNode;
    label: string;
}

export const Button = ({ size, className, color, label, children, ref }: MyButtonProps) => {
    return (
        <MyButton
            {...{
                ref,
                className,
                size,
                color,
                type: 'button',
            }}>
            {children}
            {label}
        </MyButton>
    );
};

But the extraction of the types in the build step fires the above error.

Vite config:

import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import {libInjectCss} from 'vite-plugin-lib-inject-css'
import {extname,relative,resolve} from 'path'
import {fileURLToPath} from 'node:url'
import {glob} from 'glob'

export default defineConfig({
  plugins: [react(),libInjectCss(),dts({include: ['stories/components']})],
  build: {
    copyPublicDir: false,
    lib: {
      entry: resolve(__dirname,'stories/main.ts'),
      formats: ['es'],
      name: "my-components",
    },
    rollupOptions: {
      external: ['react','react/jsx-runtime',"tailwind-variants","framer-motion",RegExp("^(@nextui-org/).+")],
      input: Object.fromEntries(
        glob.sync('stories/**/*.{ts,tsx,js,jsx}',{ignore: 'stories/**/*.stories.{ts,tsx,js,jsx}'}).map(file => [
          relative(
            'stories',
            file.slice(0,file.length-extname(file).length)
          ),
          fileURLToPath(new URL(file,import.meta.url))
        ])
      ),
      output: {
        assetFileNames: 'assets/[name][extname]',
        entryFileNames: '[name].js',
      }
    },
  },
})

And as a side note, if I create my own button using tailwind-variants everything works fine and I get type saftey in the component and no errors.

import { ReactNode, Ref } from 'react';
import { tv, type VariantProps } from 'tailwind-variants';

const button = tv({
    base: 'font-medium bg-blue-500 text-white rounded-full active:opacity-80',
    variants: {
        color: {
            primary: 'bg-blue-500 text-white',
            secondary: 'bg-purple-500 text-white',
        },
        size: {
            sm: 'text-sm',
            md: 'text-base',
            lg: 'px-4 py-3 text-lg',
        },
    },
    compoundVariants: [
        {
            size: ['sm', 'md'],
            class: 'px-3 py-1',
        },
    ],
    defaultVariants: {
        size: 'md',
        color: 'primary',
    },
});

interface ButtonProps extends VariantProps<typeof button> {
    ref?: Ref<HTMLButtonElement>;
    className?: string;
    children?: ReactNode;
    label: string;
}

export const Button = ({ size, className, color, label, children, ref }: ButtonProps) => {
    return (
        <button
            {...{
                ref,
                type: 'button',
                title: label,
                className: button({ className, color, size }),
            }}>
            {children}
            {label}
        </button>
    );
};

Edit: enter image description here

enter image description here

enter image description here

EditTwo: I have created a sandbox to show the issue. Run pnpm build-sb to create the component lib and get the error. And pnpm sb just to start it, which work.

1

There are 1 answers

14
VonC On

Check first if this is linked to nextui-org/nextui issue 1328, which mentioned:

resolved, pnpm install --hoist instead of pnpm install

If not, you will need at least to address the type inference issues that TypeScript is encountering. TypeScript is struggling to infer the types for your extended component. By explicitly defining the types, you would provide clear information to TypeScript about what to expect:

import { extendVariants, Button as NextUiButton, ButtonProps as NextUiButtonProps } from '@nextui-org/react';
import { ReactNode, Ref } from 'react';
import { type VariantProps } from 'tailwind-variants';

// Explicitly define the type for MyButtonProps
interface MyButtonProps extends VariantProps<typeof MyButton>, NextUiButtonProps {
    ref?: Ref<HTMLButtonElement>;
    className?: string;
    children?: ReactNode;
    label: string;
}

// Your existing MyButton implementation
const MyButton = extendVariants(NextUiButton, {
    // variants configuration
});

// Use the explicitly defined MyButtonProps
export const Button = (props: MyButtonProps) => {
    return <MyButton {...props} />;
};

I tried the above. I removed the node_modules and pnpm-lock, then pnpm i --hoist. But the errors are still there... nothing changed :/. This is super annoying.

I Have also tried to do this with only npm, this removes the last four errors but the top two are still there:

stories/components/Button/Button.tsx(107,7): error TS2742: The inferred type of 'WtButton' cannot be named without a reference to '../../../node_modules/@nextui-org/system-rsc/dist/types'. This is likely not portable. A type annotation is necessary. 
stories/components/Button/Button.tsx(107,7): error TS2742: The inferred type of 'WtButton' cannot be named without a reference to '../../../node_modules/@nextui-org/react-utils/dist/refs'. This is likely not portable. A type annotation is necessary

Since the errors persist even after trying the solutions like pnpm install --hoist and switching to npm, the issue might be deeply rooted in the way TypeScript is handling type inference for your extended component.

Since the issue is with the TypeScript's ability to infer types from the NextUI components, directly importing the types from NextUI's internal modules, if they are exported, might help. That is a bit of a workaround and depends on the internal structure of the NextUI package (... which might change in future updates, potentially breaking your implementation).

// Import types directly from NextUI's internal modules
import { ButtonProps as NextUiButtonProps } from '@nextui-org/system-rsc/dist/types';
import { RefProps } from '@nextui-org/react-utils/dist/refs';

// Define your extended button props using these imported types
interface MyButtonProps extends NextUiButtonProps, RefProps {
  // Your additional props
}

Another alternative approach would be to bypass type inference with any

While this is not ideal in terms of maintaining type safety, as a last resort, you can bypass TypeScript's type checking for this specific case using any. This is for testing, as it negates the benefits of TypeScript's type system.

import { extendVariants, Button as NextUiButton } from '@nextui-org/react';

// Using 'any' to bypass type inference issues
const MyButton: any = extendVariants(NextUiButton, {
  // Your variants configuration
});

// You can still define props for your custom component
interface MyButtonProps {
  // Your props
}

export const Button = (props: MyButtonProps) => {
  return <MyButton {...props} />;
};

Unfortunately, there are issues with accessing the nextUI refs folder and the :any solution will not give the correct type notation for mu variants when using the component. Like color: "suggestions".

An `import { ReactRef, assignRef, mergeRefs } from '@nextui-org/react utils/dist/refs' ; trigger:

 Cannot find module 'anextui-org/react-utils/dist/refs' or its corresponding type declarations. ts(2307)

Given the continued challenges with accessing the internal types of NextUI and the limitations of using any, you might need to explore some alternative approaches:

One way to handle this is to declare a custom type that matches the expected structure of your extended component. That approach involves creating an interface that mirrors the props of the NextUI Button and then adding your custom props.

import { Button as NextUiButton } from '@nextui-org/react';

// Declare a custom type that extends the inferred props of NextUiButton
interface CustomButtonProps extends React.ComponentProps<typeof NextUiButton> {
  // Add any additional props specific to your variant here
  color?: 'primary' | 'secondary' | 'suggestions'; // example
  // other custom props
}

const MyButton = extendVariants(NextUiButton, {
  // variants configuration
});

export const Button = (props: CustomButtonProps) => {
  return <MyButton {...props} />;
};

Another approach is to decompose the NextUI Button component and reconstruct it with your additional functionality. That method can be more complex, but allows for greater control over the component's behavior and type handling.

import { Button as NextUiButton } from '@nextui-org/react';
import React from 'react';

const MyButton = React.forwardRef<HTMLButtonElement, CustomButtonProps>((props, ref) => {
  // Rebuild the button with your custom logic and props
  // Use the NextUiButton as a base and add your customizations
  return <NextUiButton ref={ref} {...props} />;
});

// Define your custom props as needed
interface CustomButtonProps {
  // your custom props and types
}

The OP references microsoft/TypeScript issue 42873.

That issue does describe a similar problem where TypeScript fails to correctly infer types across different packages: when a package B depends on package A, and B exports a value with a type defined or referenced by A, TypeScript may produce an error similar to the one you are encountering.
That issue matches your situation, where you are extending a component from NextUI and encountering type inference problems.

One suggestion from the issue discussion includes a workaround where setting "declaration": false and "declarationMap": false in tsconfig.json resolved the problem for a user. However, this solution might not be ideal as it disables the generation of declaration files, which are essential for TypeScript's type checking in other projects that use your library.

While waiting for a resolution, you might consider simplifying your component extension and limit the use of complex type inferences or generics. Also, consider using alternative libraries or design patterns that do not require extending components from third-party libraries in such a complex manner.

Note: this issue comment references:

Importing the packages in the entry file of the library or project solved my problem.

And:

Disabling declaration and declaration map in the tsconfig for the application importing the package in my pnpm monrepo worked for me.

"declaration": false,
"declarationMap": false,

I have components build upon using only tailwind-variants.
But the nextUI lib have many great things built in like a11y for all its components and great small animations. Would be great to just extendet it.

Extending NextUI components to leverage their built-in accessibility (a11y) features and animations is indeed a good idea. Since it is tricky with TypeScript (due to type inference issues), you might instead use them within your own custom components (composition over inheritance). That way, you can utilize NextUI's features (like a11y -- accessibility -- and animations), while adding your custom styles or logic through composition.

You can also try and create HOCs (Higher-Order Components) that wrap NextUI components, adding or modifying functionality as needed. That approach allows you to reuse NextUI's features while injecting additional props or styles.

If the customization involves logic more than styles, consider using custom hooks to encapsulate and reuse this logic across components.