7

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?

Vincent
  • 4,876
  • 3
  • 44
  • 55

3 Answers3

11

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>
  );
}
brietsparks
  • 4,776
  • 8
  • 35
  • 69
0

I think this could go something like this:

import React from 'React';

type WrappedProps = {
  as: React.ReactNode,
  to: string;
}

Reference: https://github.com/typescript-cheatsheets/react-typescript-cheatsheet

niconiahi
  • 545
  • 4
  • 5
  • Unfortunately that's not want I'm looking for - `to` shouldn't be a hardcoded prop of `Wrapped`. Instead, if it's a prop of whatever component is passed to `as`, TypeScript should infer that it's also a prop of `Wrapped`. – Vincent Apr 10 '20 at 22:17
0

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>}</>
)
Steve Holgado
  • 11,508
  • 3
  • 24
  • 32
  • Unfortunately that's not want I'm looking for - `to` shouldn't be a hardcoded prop of `Wrapped`. Instead, if it's a prop of whatever component is passed to `as`, TypeScript should infer that it's also a prop of `Wrapped`. So in your examples, `Wrapped` without an element passed to `as` shouldn't accept the `to` prop. – Vincent Apr 10 '20 at 22:19
  • So you want `Wrapped` to accept the props of the component passed to the `as` prop? – Steve Holgado Apr 10 '20 at 22:45
  • Exactly. (Specifically, I want TypeScript to understand that it accepts them.) – Vincent Apr 10 '20 at 22:54
  • I have updated my answer with another possible solution. Hopefully it's of some use. – Steve Holgado Apr 12 '20 at 21:07