Search code examples
reactjstypescripttypescript-generics

React component with "as" property that points to another component and inherits it's properties too


I want to create a MyComponent with an "as" property which allows it to inherit the properties of the as component, such that I can get TypeScript typings.

e.g.

Usage with HTML elements:

<MyComponent 
  as="label" 
  htmlFor="caption" /* this should autocomplete from label */
  asdf="" /* this should give error */
  className="test"
>
  Test
</MyComponent>

With React components:

<MyComponent 
  as={MyGrid} 
  rows={4}  /* this should autocomplete from MyGrid */ 
  asdf="" /* this should give error */

>
  Test
</MyComponent>

I have this so far which works as a component, but unfortunately it doesn't inherit the types.

import { useRef, forwardRef } from "react";
import type {
  PropsWithChildren,
  ElementType,
  ComponentProps,
  ForwardedRef,
  ReactElement,
} from "react";


type MyProps<T extends ElementType> = 
  ComponentProps<T> &
  PropsWithChildren & 
  { as?: T };

function _MyComponent<T extends ElementType = "div">(
  { as, children, ...props }: MyProps<T>,
  ref: ForwardedRef<HTMLElement>,
): ReactElement<T> {
  
  const Comp = as ?? "div";

  return (
    <Comp {...props} ref={ref}>
      {children}
    </Comp>
  );
}

const MyComponent = forwardRef(_MyComponent);
export default MyComponent;

How do I need to change the Typescript so that I can get type suggestions?


Solution

  • I got it working by adding type assertion to the forwardRef call:

    export const MyComponent = forwardRef(_MyComponent) as <
      T extends ElementType = "div"
    >(
      props: MyProps<T> & { ref?: React.Ref<Element> }
    ) => ReactElement | null;
    

    Usage

    <MyComponent 
      as="label" 
      htmlFor="caption" // this autocompletes 
      asdf="" // this gives error
    >
      Test
    </MyComponent>
    
    const MyGrid = ({ rows }: { rows: number }) => (
      <div style={{ gridTemplateRows: `repeat(${rows}, 1fr)` }}></div>
    );
    
    <MyComponent as={MyGrid} rows={4}>Test</MyComponent>;
    

    Edit

    Modifying MyProps so it works for HTML elements

    type MyProps<T extends ElementType> = {
      as?: T;
    } & ComponentPropsWithoutRef<T>;