headlessui
headlessui copied to clipboard
Export component prop types
What package within Headless UI are you using?
@headlessui/react
What version of that package are you using?
v1.5.0
What browser are you using?
N/A
Reproduction URL
N/A
Describe your issue
In React + Typescript, a common pattern I follow is to wrap a library component for styling or custom behaviour before exposing it to the rest of the codebase. I like to mirror the underlying component props so that Typescript intellisense can provide the same prop hints to for the wrapper component as the underlying library component.
Here is an example of what I want:
// Wrapping a tab for custom styling. TabProps, however, does not exist;
function TemplateLibraryTab({ children, ...props }: TabProps) {
return (
<Tab
{...props}
className={({ selected }) =>
clsx("p-2 rounded", selected ? "text-gray-900" : "text-gray-500")
}
>
{children}
</Tab>
)
}
HeadlessUI does not seem to expose these props, or at least I could not find them. Would it be possible to export these prop types for this purpose? I think it is a strong use-case.
I should point out that there is a workaround for this. You can use React.ComponentProps to pull out the type from the component.
function TemplateLibraryTab({ children, ...props }: React.ComponentProps<typeof Tab>) {
return (
<Tab
{...props}
className={({ selected }) =>
clsx("p-2 rounded", selected ? "text-gray-900" : "text-gray-500")
}
>
{children}
</Tab>
)
}
This works in this use-case, but I have run into issues with it in the past. I still think that exporting prop types like other libraries is a good thing to have.
I have been doing React.ComponentProps<typeof Tab> for exactly the same use case (I wrap all the components I import) and it worked very well so far! However v1.6.0 breaks this for me. Would love to know if there is a workaround at least, otherwise will stay with v1.5.
I have been doing React.ComponentProps for exactly the same use case (I wrap all the components I import) and it worked very well so far! However v1.6.0 breaks this for me. Would love to know if there is a workaround at least, otherwise will stay with v1.5.
Yup for some reason it broke now.
Type '(<TTag extends ElementType<any> = "button">(props: Props<TTag, TabRenderPropArg, TabPropsWeControl>, ref: Ref<HTMLElement>) => ReactElement<...>) & { ...; } & { ...; }' does not satisfy the constraint 'keyof IntrinsicElements | JSXElementConstructor<any>'. Type '(<TTag extends ElementType<any> = "button">(props: Props<TTag, TabRenderPropArg, TabPropsWeControl>, ref: Ref<HTMLElement>) => ReactElement<...>) & { ...; } & { ...; }' is not assignable to type '(props: any) => ReactElement<any, any>'.
Exporting the PropTypes would probably still be the nicer solution.
Thanks to discussions/601, I got it working like this:
import { ComponentType } from 'react'
import { Dialog as HeadlessDialog } from '@headlessui/react'
type ExtractProps<T> = T extends ComponentType<infer P> ? P : T
function DialogRoot(props: ExtractProps<typeof HeadlessDialog>) {
// add your own logic here
return <HeadlessDialog {...props} />
}
You may also need to do something like this if you want the nested components to work too.
export let Dialog = Object.assign(DialogRoot, {
Backdrop: HeadlessDialog.Backdrop,
Panel: HeadlessDialog.Panel,
Overlay: HeadlessDialog.Overlay,
Title: HeadlessDialog.Title,
Description: HeadlessDialog.Description,
})
Broken in 1.6.1 for me too! Would greatly appreciate a fix 🙂
Same here with 1.4.3. Would love a fix thats less confusing.
Thanks @CapitaineToinon - i was using the original React.ComponentProps<typeof Tab> method and after upgrading to 1.6.5 ran into the length TS error above. Your new workaround worked wonders!
Plus to exporting prop types, thnx @CapitaineToinon for workaround.
+1 to exporting prop types, I'd like to use ComponentProps<typeof Dialog> like I would with all other components
+1 creating components with any sort of extensibility using headless + typescript has been a headache for me.
Since headless components are declared more as pure functions, it seems that it is now possible to extract the props using the Parameters type:
type MyPopoverButtonProps = Parameters<typeof HeadlessPopover.Button>[0];
Note the [0] at the and, since the function takes two arguments: props and ref.
For more advanced cases you can add generics:
type HeadlessUIProps = keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>;
type MyPopoverProps<T extends HeadlessUIProps> = Parameters<typeof HeadlessPopover<T>>[0];
Using Parameters<> works for some of the simple components, but it runs into issues with more complex prop configurations in components like Combobox.
For example, Combobox's value prop can either be a single value, or an array of values. The expected TS type for the value is determined by whether the multiple prop is set to true or false.
If you use the Parameters approach, TS starts to run into problems because it doesn't know which overload to use for the expected props. The combobox type declarations list the possible permutations, but there is no way to specify which permutation you want to use via Parameters

This is because the only available export is typeof ComboboxFn (which includes all possible permutations)

This means that if you try to do something like the following:
interface Option { foo: string }
// accept all but the multiple prop
type ComboboxProps<TOption extends Option> = Omit<
Parameters<typeof Combobox<TOption, React.ElementType<HTMLDivElement>>>[0],
'multiple'
>;
type MyComboboxProps<TOption extends Option> = ComboboxProps<TOption> & { myProp: string }
function MyComboboxWrapper<TOption extends Option>({value, onChange}: MyComboboxProps<TOption>) {
// value and onChange have multiple conflicting implementations
}
you will get TS errors due to the conflicting implementations.
If someone has discovered a better solution for this situations like this, I would love to hear it. I would still advocate simply exporting the Prop types so we don't need to find complex workarounds for something that should be fairly simple. I haven't seen a reason in this thread for why this couldn't be done, only workarounds which don't seem to be stable as the library is evolving. Scanning through the source, it seems like the prop types are declared for most of the components, so I feel like it should be simple enough to add the export keyword to them. I would be willing to open a PR to do this if the maintainers are not opposed to it. I would like to hear from them first before starting work on it.
+1 on this, would be extremely useful for custom-styled components in a design system where we do not want developers to directly use headless-ui components.
For Combobox, it's possible to export the props like this:
import type {ComboboxProps as BaseProps} from '@headlessui/react';
// Then
export type ComboboxProps<TValue> = BaseProps<TValue, false, false, 'div'>;
Then you can probably pass generic over the 4 arguments, but trying that out increases the complexity of the component considerably.
Hope this help, and hopefully others can also share how they've done their component!
@RobinMalfait I hate to pester you, but I would love it if we got some more clarity around the plan for this. I can see in a linked issue that you "want to simplify and export the types". Is this something that is currently being worked on, or are you facing blockers here?
Don't want to put pressure on you here, but just knowing what your thoughts are around this and where you are at would be great.
+1, this issue has been really bugging me. In my case I just want to use the props Dialog exposes to build my own compound components
Related question - I want to get the prop types for Dialog.Panel to use on a wrapper component, but using React.ComponentPropsWithoutRef<typeof Dialog.Panel> like suggested in the React-Typescript cheatsheet produces a {} type.
How should I be getting the type for Dialog.Panel props?
UPDATE: Jumped the gun - apparently using type ExtractProps<T> = T extends ComponentType<infer P> ? P : T works, but className and style is just typed as any in ExtractProps<typeof Dialog.Panel>, so I was misled to believe prop types weren't working.
In the next version of Headless UI we're exporting XXXProps types for each component. They are all generic because you can change which tag / component our components render with (as well as some types like Combobox have additional generics for values and other types). Hopefully this solution will work as you expect.
In the meantime you can test out these changes by installing our insiders build:
npm install @headlessui/react@insiders
Thanks for the patience on this while we figured stuff out. ✨
Thanks for the update @thecrypticace and thanks to the rest of the team for this feature and all your work on HeadlessUI!
But this one was working before. What I'm doing here is I'm extracting the props from the component itself:
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>
);
};
Hi there, is there any place where we can gran types for Vue?
Does anyone have a working example of using the now-exported XXXProps types to make a wrapper component? I'm looking to use a RadioGroup under the hood for another type of component and haven't figured out the right syntax yet. The ExtractProps method doesn't seem to work.
EDIT: Nevermind, the ExtractProps method does seem to work. I ran into the same mistake above where className is typed as any, so thought it wasn't working. But I'd like to use the XXXProps type if possible still.
@mattfelten You can do something like this now:
import { RadioGroup, RadioGroupProps } from "@headlessui/react";
function MyRadioGroup(props: RadioGroupProps<"div", string>) {
return <RadioGroup {...props} />;
}
Thanks @valtism ! That did work. Now that I understand what those two arguments are for, I slightly modified it to not be specific and it seems to still work.
import { RadioGroup, RadioGroupProps } from "@headlessui/react";
import { ComponentPropsWithoutRef, ElementType } from "react";
interface CustomWrapperProps extends RadioGroupProps<ElementType, ComponentPropsWithoutRef<"input">["value"]> { ... }