Search code examples
reactjstypescriptemotion

How to type a React component that accepts another component as a prop, and all of that component's props?


I've got a React component <Wrapper> that I can use as follows:

// Assuming Link is a component that takes a `to` prop: <Link to="/somewhere">label</Link>
<Wrapped as={Link} to="/somewhere">label</Wrapped>

If no as prop is passed, it will assume an <a>. But if a component is passed to as, all props that are valid for that component should now also be valid props of Wrapped.

Is there a way to type this in TypeScript? I was currently thinking along these lines:

type Props<El extends JSX.Element = React.ReactHTMLElement<HTMLAnchorElement>> = { as: El } & React.ComponentProps<El>;
const Wrapped: React.FC<Props> = (props) => /* ... */;

However, I'm not sure whether JSX.Element and/or React.ComponentProps are the relevant types here, and this does not compile because El can not be passed to ComponentProps. What would the correct types there be, and is something like this even possible?


Solution

  • The tyes you need are ComponentType and ElementType.

    import React, { ComponentType, ElementType, ReactNode } from 'react';
    
    type WrappedProps <P = {}> = { 
      as?: ComponentType<P> | ElementType
    } & P
    
    function Wrapped<P = {}>({ as: Component = 'a', ...props }: WrappedProps<P>) {
      return (
        <Component {...props} />
      );
    }
    

    With that, you are able to do:

    interface LinkProps {
      to: string,
      children: ReactNode,
    }
    function Link({ to, children }: LinkProps) {
      return (
        <a href={to}>{children}</a>
      );
    }
    
    function App() {
      return (
        <div>
          <Wrapped<LinkProps> as={Link} to="/foo">Something</Wrapped>
          <Wrapped as="div" style={{ margin: 10 }} />
          <Wrapped />
        </div>
      );
    }