Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle props injected by HOC in React with Typescript?

I created a simple HOC that inject a method translate in a component.

export interface IMessageProps {
  translate: (key: string) => string;
}

export const message = <P extends object>(
  Component: React.ComponentType<P & IMessageProps>
): React.SFC<P & IMessageProps> => (props: P) => {

  const translate = (key: string): string => messages[key];

  return <Component {...props} translate={translate}/>;
};

Usage:

class MyComponent extends React.Component<IMessageProps, {}> {
  render() {
    return (
      <>{this.props.translate('hello.world')}</>
    );
  }
}

export default message(MyComponent);

The issue come when i want to call my component <MyComponent/> because tsc complains that the property translate is not passed to MyComponent and expect something like <MyComponent translate={...}/>.

Type '{}' is not assignable to type 'IntrinsicAttributes & IMessageProps & { children?: ReactNode; }'.
  Type '{}' is not assignable to type 'IMessageProps'.
    Property 'translate' is missing in type '{}'.

So my question is: how to bypass this fake error ? I don't want to make translate optional in IMessageProps because tslint will complains about Cannot invoke an object which is possibly 'undefined'.

like image 806
Alexandre Annic Avatar asked Jun 28 '18 13:06

Alexandre Annic


Video Answer


1 Answers

Edit

Typescript 3.2 breaks the code below. Until 3.2 spread operations with generic type parameters were not allowed except for jsx tags and were not very tightly checked there. This issue changes this. Spread operations are not more tightly checked and the this breaks out code. The simplest adjustment we can make is to use a type assertion on props :

export const message = <P extends IMessageProps>(
    Component: React.ComponentType<P>
): React.SFC<Pick<P, Exclude<keyof P, keyof IMessageProps>>> => (props: Pick<P, Exclude<keyof P, keyof IMessageProps>>) => {

    const translate = (key: string): string => messages[key];

    return <Component {...props as P} translate={translate} />;
};

Before 3.2

You can just exclude the properties of IMessageProps from the returned SCF using Pick to pick properties from P and Exclude to exclude the keys of IMessageProps

export interface IMessageProps {
    translate: (key: string) => string;
}

export const message = <P extends IMessageProps>(
    Component: React.ComponentType<P>
): React.SFC<Pick<P, Exclude<keyof P, keyof IMessageProps>>> => (props: Pick<P, Exclude<keyof P, keyof IMessageProps>>) => {

    const translate = (key: string): string => messages[key];

    return <Component {...props} translate={translate} />;
};


class MyComponent extends React.Component<IMessageProps, {}> {
    render() {
        return (
            <>{this.props.translate('hello.world')}</>
        );
    }
}

const MyComponentWrapped = message(MyComponent);

let d = <MyComponentWrapped /> // works

3.5 and above

You can use Omit<P, keyof IMessageProps> instead of Pick<P, Exclude<keyof P, keyof IMessageProps>>

like image 101
Titian Cernicova-Dragomir Avatar answered Sep 29 '22 04:09

Titian Cernicova-Dragomir