Search code examples
reactjstypescripttypescript-genericsreact-props

How to write correct TypeScript types for slot component props when using Component slot


I have a React component that accepts a SlotComponent prop as well as a slotComponentProps prop for props that are to be passed to the slot component:

interface MyComponentProps<T extends React.ElementType> {
  SlotComponent: T
  slotComponentProps: React.ComponentProps<T>
}

function MyComponent<T extends React.ElementType>({
  SlotComponent,
  slotComponentProps,
}: MyComponentProps<T>): React.ReactNode {
  return (
    <div>
      <SlotComponent {...slotComponentProps} />
      // Other things go here.
    </div>
  )
}

This partially works, but isn't as flexible as I would like it to be. The issues I have are:

  1. If I try to make SlotComponent optional and default it to something like 'button', I get the TypeScript error:
TS2322: Type '"button"' is not assignable to type 'T'.
'"button"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ElementType<any, keyof IntrinsicElements>'.

SlotComponent = 'button',
~~~~~~~~~~~~~
  1. If I try to make slotComponentProps optional, then I get this TypeScript error:
TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes<T, any>'.

<SlotComponent {...slotComponentProps} />
 ~~~~~~~~~~~~~
  1. If I try to make slotComponentProps optional and add {} as a default, I get this TypeScript error:
TS2322: Type '{}' is not assignable to type 'ComponentProps<T>'.

slotComponentProps = {},
~~~~~~~~~~~~~~~~~~

How can I update my types to allow me to achieve these changes?


Solution

  • Not sure which level of flexibility you would like to achieve, but I figured out the following solution, which allows the following:

    1. SlotComponent can be skipped. DEFAULT_ELEMENT_TYPE will be used instead. slotComponentProps still can be provided, type is controlled.
    2. slotComponentProps can be skipped.
    3. Both can be skipped
    4. Both can be provided. Type of slotComponentProps is checked to be compatible with props of the SlotComponent

    The solution is to make both parameters optional and default them in the function body in case of absence. Default generic type is used to control type of slotComponentProps when SlotComponent is not specified.

    interface MyComponentProps<T extends React.ElementType> {
      SlotComponent?: T
      slotComponentProps?: React.ComponentProps<T>
    }
    
    const DEFAULT_ELEMENT_TYPE = 'button' as const;
    type DefaultElementTypeType = typeof DEFAULT_ELEMENT_TYPE;
    
    function MyComponent<T extends React.ElementType = DefaultElementTypeType>({SlotComponent, slotComponentProps}: MyComponentProps<T>): React.ReactNode {
        const Component: React.ElementType = SlotComponent ?? DEFAULT_ELEMENT_TYPE;
      return (
        <div>
          <Component {...slotComponentProps} />
          // Other things go here.
        </div>
      )
    }