3

I am trying to write a higher order function for React in typescript that: (1) Requires a certain properties on the component being wrapped (2) Allows for the wrapped components properties to be set on the wrapper (3) Has properties specific to the wrapper

I mostly have things working, but when I go to set default properties on the anonymous class that wraps my component I get an error from typescript that I have not been able to resolve.

The error I get is:

src/withContainer.tsx:33:3 - error TS2322: Type 'typeof (Anonymous class)' is not assignable to type 'ComponentClass<P & ContainerProps, any>'.
  Types of property 'defaultProps' are incompatible.
    Type '{ loading: Element; }' is not assignable to type 'Partial<P & ContainerProps>'.

 33   return class extends React.Component<P & ContainerProps, ContainerState> {
      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 34     // Why does typescript say this is an error?
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... 
 80     }
    ~~~~~
 81   };
    ~~~~

Here is an example of what I am trying to do:

import * as React from "react";

/**
 * Properties specific to the container component.
 */
export interface ContainerProps {
  /* eslint-disable @typescript-eslint/no-explicit-any */
  loading: React.ReactElement<any>;
}

export interface ContainerState {
  data: {};

  initialized: boolean;
}

/**
 * Components being wrapped need a putData function on them.
 */
export interface PuttableProps {
  /**
   * Put data into state on the parent component.
   *
   * @param data Data to be put into state.
   */
  putData(data: object): void;
}

/* eslint-disable max-lines-per-function */
export function withContainer<P>(
  WrappedComponent: React.ComponentType<P & PuttableProps>
): React.ComponentClass<P & ContainerProps> {
  return class extends React.Component<P & ContainerProps, ContainerState> {
    // Why does typescript say this is an error?
    static defaultProps = {
      loading: <React.Fragment />
    };

    state: ContainerState = {
      initialized: false,
      data: {}
    };

    /**
     * After mounting, simulate loading data and mark initialized.
     */
    componentDidMount(): void {
      // Simulate remote data load, 2 minutes after we mounted set initialized to true
      setTimeout(() => {
        this.setState({
          initialized: true
        });
      }, 2000);
    }

    /**
     * Set data as state on the parent component.
     */
    private putData = (data: object): void => {
      this.setState({ data });
    };

    /**
     * Render the wrapped component.
     */
    render(): React.ReactNode {
      // If we haven't initialized the document yet, don't return the component
      if (!this.state.initialized) {
        return this.props.loading;
      }

      // Whatever props were passed from the parent, our data and our putData function
      const props = {
        ...this.props,
        ...this.state.data,
        putData: this.putData
      };

      return <WrappedComponent {...props} />;
    }
  };
}

export class ClickCounter extends React.Component<
  PuttableProps & { count: number },
  {}
> {
  static defaultProps = {
    count: 0
  };

  increment = () => {
    this.props.putData({ count: this.props.count + 1 });
  };

  render(): React.ReactNode {
    return (
      <div>
        <h1>{this.props.count}</h1>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

And it can be leveraged like this:

import React from "react";
import ReactDOM from "react-dom";
import { withContainer, ClickCounter } from "./withContainer";

const WrappedClickCounter = withContainer(ClickCounter);
const component = (
  <WrappedClickCounter count={0} loading={<div>Loading...</div>} />
);

ReactDOM.render(component, document.getElementById("root"));

I've tried a few variations of this, including having P extend ContainerProps but nothing seems to work.

I am using typescript 3.3.1.

2 Answers 2

1

As the error says, your defaultProps don't match your ContainerProps type. Specifically, in ContainerProps, you have { isLoading: ReactElement<any> }, but your defaultProps has type { loading: Element }.

I'd recommend you define ContainerProps as this instead:

import type { ReactNode } from 'react';

export interface ContainerProps {
  isLoading: ReactNode
}
Sign up to request clarification or add additional context in comments.

Comments

0

Did you ever solve this? I on the line where you return the component, have you tried using Partial?:

return class extends React.Component<P & ContainerProps...

Can you try this instead?

return class extends React.Component<Partial<P & ContainerProps>....

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.