user1876909
user1876909

Reputation: 143

Infer type of props based on component passed also as prop

Is it possible to infer correct types for props from an unknown component passed also as a prop?

In case of known component (that exists in current file) I can get props:

type ButtonProps = React.ComponentProps<typeof Button>;

But if I want to create a generic component Box that accepts a component in as prop and the component's props in props prop. The component can add some default props, have some behavior, it doesn't matter. Basically its similar to higher-order components, but its dynamic.

import React from "react";

export interface BoxProps<TComponent> {
  as?: TComponent;
  props?: SomehowInfer<TComponent>; // is it possible?
}

export function Box({ as: Component, props }: BoxProps) {
  // Note: it doesn't have to be typed within the Box (I can pass anything, I can control it)
  return <Component className="box" title="This is Box!" {...props} />;
}

function MyButton(props: {onClick: () => void}) {
  return <button className="my-button" {...props} />;
}

// usage:

function Example() {
  // I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type. 
  return (
    <div>
      <Box
        as={MyButton}
        props={{
          onClick: () => {
            console.log("clicked");
          }
        }}
      >
        Click me.
      </Box>
    </div>
  );
}

requirements:

Upvotes: 4

Views: 3868

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249506

You can use the predefined react type ComponentProps to extract the prop types from a component type.

import React from "react";

export type BoxProps<TComponent extends React.ComponentType<any>> = {
  as: TComponent;
  props: React.ComponentProps<TComponent>;
}

export function Box<TComponent extends React.ComponentType<any>>({ as: Component, props }: BoxProps<TComponent>) {  
  return <div className="box" title="This is Box!">
    <Component  {...props} />;
  </div>
}

function MyButton(props: {onClick: () => void}) {
  return <button className="my-button" {...props} />;
}

// usage:

function Example() {
  // I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type. 
  return (
    <div>
      <Box
        as={MyButton}
        props={{ onClick: () => { } }}
      ></Box>
    </div>
  );
}

Playground Link

Depending on you exact use case the solution might vary, but the basic idea is similar. You could for example turn the type around a little bit and take in the props as the type parameter for the BoxProps. That way you can constrain the component props to have some specific properties you can supply inside the Box component:

export type BoxProps<TProps extends {title: string}> = {
  as: React.ComponentType<TProps>;
} & {
  props: Omit<TProps, 'title'>;
}

export function Box<TProps extends {title: string}>({ as: Component, props }: BoxProps<TProps>) {  
  return <div className="box" title="This is Box!">
    <Component title="Title from box" {...props as TProps} />;
  </div>
}

Playground Link

If you want to take in intrinsic tags, you can also add keyof JSX.IntrinsicElements to the TComponent constraint:

export type BoxProps<TComponent extends React.ComponentType<any> | keyof JSX.IntrinsicElements> = {
  as: TComponent;
  props: React.ComponentProps<TComponent>;
}

export function Box<TComponent extends React.ComponentType<any>| keyof JSX.IntrinsicElements>({ as: Component, props }: BoxProps<TComponent>) {  
  return <div className="box" title="This is Box!">
    <Component  {...props} />;
  </div>
}

Playground Link

Upvotes: 5

Related Questions