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

Viewed 3793

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?

3 Answers

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>
  );
}

This might work for you:

type Props = {
  as?: React.FC<{ to: string, children: any }>,
  to: string,
  children: any
}

const Wrapped: React.FC<Props> = ({ as: El, to, children }) => (
  El
    ? <El to={to}>{children}</El>
    : <a href={to}>{children}</a>
)

Then you can use with an as prop:

<Wrapped as={Link} to="/somewhere">label</Wrapped>

...or without to default to the <a> element:

<Wrapped to="/somewhere">label</Wrapped>




Further to your comment below, if you want Wrapped to accept the props of the component passed to the as prop then I'm not sure how to do this or if it's possible.

However, you could try something like this:

<Wrapped as={<Link to="somewhere">{label}</Link>} />

...so passing Link to the as prop in this way means that you don't need to pass the props to Wrapped.

type Props = {
  as?: React.ReactNode
  children: any
}

const Wrapped: React.FC<Props> = ({ as, children }) => (
  <>{ as ? as : <a href="/somewhere">{children}</a>}</>
)
Related