headlessui icon indicating copy to clipboard operation
headlessui copied to clipboard

Prop merging wipes out typing for included HeadlessUI props

Open briavicenti opened this issue 3 years ago • 2 comments

What package within Headless UI are you using?

@headlessui/react

What version of that package are you using?

v1.6.0

What browser are you using?

N/A (Typescript issue)

Reproduction URL

https://codesandbox.io/s/optimistic-robinson-djo0yp?file=/src/App.tsx

^ Note the Typescript error present in the styled-component for Switch. You can swap the version of @headlessui/react to v.1.5.0 to see that v1.6.0 introduced the issue.

Describe your issue

This PR altered how prop types were merged for components, and included a change to the prop typings in the render util. That change limits the ability for prop types for the HeadlessUI components are picked up by styled-components, leading to new type errors when trying to utilize the included props (ex: checked on a Switch component) inside of a styled-component declaration.

briavicenti avatar Apr 25 '22 22:04 briavicenti

This is blocking us from upgrading as well. Any help would be appreciated.

anark avatar Jun 02 '22 20:06 anark

I am having a similar issue with Tabs, I also saw a StackOverflow question related to this: https://stackoverflow.com/questions/73333525/headless-ui-disclosure-not-working-with-styled-components

I noticed the issue goes away when I remove the ref from the type:

Current code:

export declare let Tab: (<TTag extends React.ElementType<any> = "button">(props: Props<TTag, TabRenderPropArg, TabPropsWeControl>, ref: Ref<HTMLElement>) => React.ReactElement<any, string>) & {

Without the ref:

export declare let Tab: (<TTag extends React.ElementType<any> = "button">(props: Props<TTag, TabRenderPropArg, TabPropsWeControl>) => React.ReactElement<any, string>) & {

But I understand we need the ref so I also checked with React.ForwardRefExoticComponent which seems to work as well:

type TabType<TTag extends React.ElementType<any> = "button">= React.ForwardRefExoticComponent<(Props<TTag, TabRenderPropArg, TabPropsWeControl>) & React.RefAttributes<HTMLElement>>
export declare let Tab: TabType & {

Hopefully, improving the type of the forwardRefWithAs helper should fix this issue. https://github.com/tailwindlabs/headlessui/blob/b301f04c7747fcdebc9fcc380dee60c4393c7c2d/packages/%40headlessui-react/src/utils/render.ts#L259

markcnunes avatar Aug 24 '22 15:08 markcnunes

Seeing a similar issue (I think) with RadioGroup: https://codesandbox.io/s/headless-ui-types-issue-oi8bpz?file=/src/App.tsx

Doesn't happen with Listbox though.

will-stone avatar Nov 29 '22 13:11 will-stone

Any news here? This is blocking us from upgrading

Alagaesia93 avatar Feb 06 '23 11:02 Alagaesia93

A fix for this has been included in #2282. In some cases it will "just work":

Screen Shot 2023-02-20 at 12 42 31

There's some problem around inference with generics and props that can prevent inference from working in all cases — even on fairly simple components. I think this behavior depends on TypeScript version as well. If you run into a case where it doesn't you can use the new {XXX}Props types to type things directly instead of them being inferred as any:

Screen Shot 2023-02-20 at 12 43 15

This will be in the next tagged release of Headless UI but in the meantime you can give it a try today with our insiders build:

npm install @headlessui/react@insiders

Thanks for your patience on this one — it's much appreciated! ✨

thecrypticace avatar Feb 20 '23 17:02 thecrypticace

Can't we just extract the props like this?

import React, { ComponentType } from 'react';
import { Menu } from '@headlessui/react';
import { Button } from '../Button/Button';

type ExtractProps<T> = T extends ComponentType<infer P> ? P : T;

export type MenuButtonType = ExtractProps<typeof Menu.Button>;

export const MenuButton: React.FC<MenuButtonType> = ({ children, ...props }) => {
  return (
    <Menu.Button as={Button} {...props}>
      {children}
    </Menu.Button>
  );
};

rmlevangelio avatar Mar 01 '23 12:03 rmlevangelio

@rmlevangelio For a type like Menu.Button that does indeed work but not for more complex components and props types. In general our props types require you to be more explicit about the types you're using — which in general lines up with how TypeScript infers behavior based on the props already specified. For example, if you have a more complex type with more generics like Combobox which enables changes in behavior based on the value of props then you will get types that can be incorrect:

Screen Shot 2023-03-01 at 09 55 56

The type of the value prop can differ based on whether or not either one (or both) of nullable and multiple are true. I'm using any here to signal that both are possible. infer U in Typescript has limitations around generics + overloads that can require conditional types to resolve.

thecrypticace avatar Mar 01 '23 15:03 thecrypticace

@thecrypticace I see. It's like the react-select implementation wherein you can define your own shape. However, the ExtractProps approach doesn't work now after the 1.7.12 release.

rmlevangelio avatar Mar 04 '23 16:03 rmlevangelio