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]
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;