Search code examples
reactjstypescriptreact-router-domreact-testing-libraryreact-typescript

Cannot satisfy wrapper parameter for react testing library render function using TypeScript


I'm trying to build a convenience function that allows me to pass a react component and have it render the component with all of the options and route specifications that I want that I pass it wrapped around a <BrowserRouter> for unit testing.

The problem is when I try to pass Wrapper function into react testing library's render() (see rtlRender() below), I'm getting an error from TypeScript stating the following

No overload matches this call. Overload 1 of 2, '(ui: ReactElement<any, string | JSXElementConstructor>, options: RenderOptions<Queries, HTMLElement>): RenderResult<...>', gave the following error. Type '({ children }: Props) => JSX.Element' is not assignable to type 'ComponentType<{}> | undefined'. Type '({ children }: Props) => JSX.Element' is not assignable to type 'FunctionComponent<{}>'. Types of parameters '__0' and 'props' are incompatible. Type '{ children?: ReactNode; }' is not assignable to type 'Props'. Types of property 'children' are incompatible. Type 'ReactNode' is not assignable to type 'ReactChildren'. Type 'undefined' is not assignable to type 'ReactChildren'. Overload 2 of 2, '(ui: ReactElement<any, string | JSXElementConstructor>, options?: Omit<RenderOptions<typeof import("/Users/myuser/dev/myapp/container/node_modules/@testing-library/dom/types/queries"), HTMLElement>, "queries"> | undefined): RenderResult<...>', gave the following error. Type '({ children }: Props) => JSX.Element' is not assignable to type 'ComponentType<{}> | undefined'. Type '({ children }: Props) => JSX.Element' is not assignable to type 'FunctionComponent<{}>'.ts(2769) index.d.ts(41, 3): The expected type comes from property 'wrapper' which is declared here on type 'RenderOptions<Queries, HTMLElement>' (property) RenderOptions<Queries, HTMLElement>.wrapper?: React.ComponentType<{}> | undefined

Here's my code

import React from 'react';
import { Queries, render as rtlRender } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { RenderResult } from '@testing-library/react/types';

/* Type Definitions */
type Route = {
  state: Record<string, unknown>;
  title: string;
  location: string;
};
type Props = {
  children: React.ReactChildren;
};

/* Helper Functions */
export function render(ui: JSX.Element, options: { route: Route }): RenderResult<Queries, HTMLElement> {
  const { route } = options;
  window.history.pushState(route?.state ?? null, route?.title ?? 'Root', route?.location ?? '/');

  const Wrapper = ({ children }: Props) => {
    return <BrowserRouter>{children}</BrowserRouter>;
  };

  return rtlRender(ui, { wrapper: Wrapper, ...options });
}

From what I understand, it's telling me that whatever Wrapper() returns doesn't fit the description of a React class component or functional component. This seems weird to me because you can tell from the code, Wrapper() should return a functional component.

I tried to assign Wrapper() to type React.FC<Props>, but TypeScript states that React.FC<Props> is not of type React.ComponentType which made me even more confused.

I'm still new to TypeScript so throwing in all these React types is confusing me even more. I'm hoping for some guidance on this.


Solution

  • React.FC type is alias of React.FunctionComponent type. The props of React.FunctionComponent is PropsWithChildren type which has children?: ReactNode property. You don't need to create your own Props type.

    The options parameter type of render function is:

    export interface RenderOptions<Q extends Queries = typeof queries> {
      container?: Element
      baseElement?: Element
      hydrate?: boolean
      queries?: Q
      wrapper?: React.ComponentType
    }
    

    There is no route property, I think it's for the custom render function. But you need to use Intersection Types so that you can pass the options to render function of RTL.

    type FC<P = {}> = FunctionComponent<P>;
    
    interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }
    
    type PropsWithChildren<P> = P & { children?: ReactNode };
    

    The default generic parameter type for RenderResult is typeof queries, NOT Queries. This is the Queries interface:

    export interface Queries {
        [T: string]: Query;
    }
    

    It's totally different with @testing-library/dom/types/queries.d.ts.

    import React from 'react';
    import { queries, render as rtlRender, RenderOptions } from '@testing-library/react';
    import { BrowserRouter } from 'react-router-dom';
    import { RenderResult } from '@testing-library/react/types';
    
    /* Type Definitions */
    type Route = {
      state: Record<string, unknown>;
      title: string;
      location: string;
    };
    
    /* Helper Functions */
    export function render(
      ui: React.ReactElement,
      options: { route: Route } & Omit<RenderOptions, 'queries'>
    ): RenderResult<typeof queries> {
      const { route, ...rest } = options;
      window.history.pushState(route?.state ?? null, route?.title ?? 'Root', route?.location ?? '/');
    
      const Wrapper: React.FC = ({ children }) => {
        return <BrowserRouter>{children}</BrowserRouter>;
      };
    
      return rtlRender(ui, { wrapper: Wrapper, ...rest });
    }