Search code examples
reactjstypescripttypescript-typingshigher-order-componentstypescript-types

TypeScript React HOC return type compatible with decorator


I'm using React Native with TypeScript. I have written a HOC that I like to use as a decorator to give components a badge:

import React, { Component, ComponentClass, ReactNode } from "react";
import { Badge, BadgeProps } from "../Badge";

function withBadge<P>(
  value: number,
  hidden: boolean = value === 0
): (WrappedComponent: ComponentClass<P>) => ReactNode {
  return (WrappedComponent: ComponentClass<P>) =>
    class BadgedComponent extends Component<P> {
      render() {
        return (
          <React.Fragment>
            <WrappedComponent {...this.props} />
            {!hidden && <Badge value={value} />}
          </React.Fragment>
        );
      }
    };
}

export default withBadge;

The problem is now that when I try to use this component as a decorator like this:

import React, { PureComponent } from "react";
import { Icon } from "react-native-elements";
import { getIconName } from "../../services/core";
import withBadge from "../WithBadge/withBadge";
import styles from "./styles";

@withBadge(1)
export default class BadgedCart extends PureComponent {
  render() {
    return (
      <Icon
        type="ionicon"
        name={getIconName("cart")}
        containerStyle={styles.iconRight}
        onPress={() => {
          // Nothing.
        }}
      />
    );
  }
}

I get the error:

[ts]
Unable to resolve signature of class decorator when called as an expression.
  Type 'null' is not assignable to type 'void | typeof BadgedCart'. [1238]

I've tried different other return types like JSX.Element or ReactElement<any>, but the only one that works is just any which defeats the purpose of TypeScript. What return type are Higher Order Components supposed to have?

Edit: When I change the return type (like Praveen suggested) to typeof PureComponent the error for @withBadge(1) changes to:

[ts]
Unable to resolve signature of class decorator when called as an expression.
  Type 'typeof PureComponent' is not assignable to type 'typeof BadgedOrders'.
    Type 'PureComponent<any, any, any>' is not assignable to type 'BadgedOrders'.
      Types of property 'render' are incompatible.
        Type '() => ReactNode' is not assignable to type '() => Element'.
          Type 'ReactNode' is not assignable to type 'Element'.
            Type 'undefined' is not assignable to type 'Element'. [1238]

If I try to change it just to PureComponent

    class BadgedComponent extends Component<P> {
      render() {
        return (
          <React.Fragment>
            <WrappedComponent {...this.props} />
            {!hidden && <Badge value={value} />}
          </React.Fragment>
        );
      }
    };

Throws the error:

[ts]
Type '(WrappedComponent: ComponentClass<P, any>) => typeof BadgedComponent' is not assignable to type '(WrappedComponent: ComponentClass<P, any>) => PureComponent<{}, {}, any>'.
  Type 'typeof BadgedComponent' is not assignable to type 'PureComponent<{}, {}, any>'.
    Property 'context' is missing in type 'typeof BadgedComponent'. [2322]

Solution

  • The first issue is that P is not in a position where it can be inferred. Since it's on withBadge and that call will not contain it.

    The second problem is that a class decorator must return void or the same type as the input class:

    declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
    

    This means that the decorator signature can't be (WrappedComponent: ComponentClass<P>) => ReactNode, as it returns ReactNode. It can't even be (WrappedComponent: ComponentClass<P>) => ComponentClass<P> as that does not return the exact same as what is passed in. A solution is to lie to the compiler a bit, and declare we return void when we actually return a new class. This will not make much of a difference at runtime:

    class Badge extends React.Component<{ value: number }> { 
    
    }
    
    function withBadge(
    value: number,
    hidden: boolean = value === 0
    ): <P extends object>(WrappedComponent: ComponentClass<P>) => void {
    return <P extends object>(WrappedComponent: ComponentClass<P>) =>
        class BadgedComponent extends Component<P> {
        render() {
            return (
            <React.Fragment>
                <WrappedComponent {...this.props} />
                {!hidden && <Badge value={value} />}
            </React.Fragment>
            );
        }
        };
    }
    
    @withBadge(1)
    export default class BadgedCart extends PureComponent {
        render() {
            return (
            <div />
            );
        }
    }
    

    You might consider ditching the decorator approach in favor of a simple function call:

    class Badge extends React.Component<{ value: number }> { 
    
    }
    
    function withBadge(
        value: number,
        hidden: boolean = value === 0
    ): <P extends object>(WrappedComponent: ComponentClass<P>) => ComponentClass<P> {
    return <P extends object>(WrappedComponent: ComponentClass<P>) =>
        class BadgedComponent extends Component<P> {
            render() {
                return (
                <React.Fragment>
                    <WrappedComponent {...this.props} />
                    {!hidden && <Badge value={value} />}
                </React.Fragment>
                );
            }
        };
    }
    
    const  BadgedCart = withBadge(1)(class extends PureComponent {
        render() {
            return (
            <div />
            );
        }
    });
    export default BadgedCart;