Home > database >  How can a function correctly accept a React component and its props as arguments in TypeScript and r
How can a function correctly accept a React component and its props as arguments in TypeScript and r

Time:10-22

I test some components that wrap children and want to make a function that renders them with some children added. How can we do this with Typescript without losing type-safety? See comments in the code below:

import * as React from "react";
import { render } from "@testing-library/react";

//function renderContainer<Props extends {children: React.ReactNode}>
function renderContainer<Props>(
  Container: React.ComponentType<Props>,
  props?: Props
) {
  return render(
    /*
      Currently, we have this error here:
     
      "Type '{ children: Element; }'is not assignable to type 'Props'.
      'Props' could be instantiated with an arbitrary type which could
      be unrelated to '{ children: Element; }"
     
      If we constrain the`Props` type like <Props extends {children: React.ReactNode}>,
      we get this error:
     
      "Type '{ children: Element; }' is not assignable to type 'Props'.
      '{ children: Element; }' is assignable to the constraint of type 'Props',
      but 'Props' could be instantiated with a different subtype of constraint
      '{ children: ReactNode; }'"
    */
    <Container {...props}>
      <div>Child</div>
    </Container>
  );
}

type CustomComponentProps = {
  children?: React.ReactNode;
  prop1: number;
  prop2?: string;
};
const CustomComponent = ({ prop1, prop2, children }: CustomComponentProps) => (
  <div>{children}</div>
);

// Expected behavior:
renderContainer(CustomComponent, { prop1: 1 }); // ok
renderContainer(CustomComponent, { prop1: 1, prop2: "string" }); // ok
renderContainer(CustomComponent, { prop3: 3 }); // error

Codesandbox

CodePudding user response:

Actually, the easier way to resolve this problem is to think the other way around, and setting the generic in renderContainer as the Component type it self, instead of the props.

type ReactComponent = JSXElementConstructor<any>;

type CustomComponentProps = {
  children?: React.ReactNode;
  prop1: number;
  prop2?: string;
};

const CustomComponent = ({ prop1, prop2, children }: CustomComponentProps) => (
  <div>{children}</div>
);


declare function renderContainer<T extends ReactComponent>(container: T,  props: React.ComponentProps<T>): React.ReactNode
renderContainer(CustomComponent, { prop1: 1 }); // ok
renderContainer(CustomComponent, { prop1: 1, prop2: "string" }); // ok
renderContainer(CustomComponent, { prop3: 3 }); // error

This way is more natural, because you are defining what will be the component first and then the props will be inferred by TypeScript, you don't even have to specifically pass any generic into the function.

CodePudding user response:

Here is a working version.

Props needs to extend PropsWithChildren<any>, like so:

import * as React from "react";
import { render } from "@testing-library/react";

function renderContainer<Props extends React.PropsWithChildren<any>>(
  //function renderContainer<Props>(
  Container: React.ComponentType<Props>,
  props?: Props
) {
  return render(
    <Container {...props}>
      <div>Child</div>
    </Container>
  );
}

type CustomComponentProps = {
  children?: React.ReactNode;
  prop1: number;
  prop2?: string;
};
const CustomComponent = ({ prop1, prop2, children }: CustomComponentProps) => (
  <div>{children}</div>
);

// Expected behavior:
renderContainer(CustomComponent, { prop1: 1 }); // ok
renderContainer(CustomComponent, { prop1: 1, prop2: "string" }); // ok
renderContainer(CustomComponent, { prop3: 3 }); // error

/* Argument of type '({ prop1, prop2, children }: CustomComponentProps) => JSX.Element' is not assignable to parameter of type 'ComponentType<{ prop3: number; }>'.
  Type '({ prop1, prop2, children }: CustomComponentProps) => JSX.Element' is not assignable to type 'FunctionComponent<{ prop3: number; }>'.
    Types of parameters '__0' and 'props' are incompatible.
      Property 'prop1' is missing in type 'PropsWithChildren<{ prop3: number; }>' but required in type 'CustomComponentProps'.ts(2345) /*


If you want the error on the props instead of CustomComponent:

import * as React from "react";
import { render } from "@testing-library/react";

function renderContainer<Props extends React.PropsWithChildren<any>>(
  //function renderContainer<Props>(
  Container: React.ComponentType<Props>,
  props?: Props
) {
  return render(
    <Container {...props}>
      <div>Child</div>
    </Container>
  );
}

type CustomComponentProps = {
  children?: React.ReactNode;
  prop1: number;
  prop2?: string;
};
const CustomComponent: React.ComponentType<CustomComponentProps> = ({ prop1, prop2, children }) => (
  <div>{children}</div>
);

// Expected behavior:
renderContainer(CustomComponent, { prop1: 1 }); // ok
renderContainer(CustomComponent, { prop1: 1, prop2: "string" }); // ok
renderContainer(CustomComponent, { prop3: 3 }); // error

/* Argument of type '{ prop3: number; }' is not assignable to parameter of type 'CustomComponentProps'.
Object literal may only specify known properties, but 'prop3' does not exist in type 'CustomComponentProps'. Did you mean to write 'prop1'? /*


codesandbox

  • Related