Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flow: is not a polymorphic type

i just integrated flow for the first time to check my javascript sources statically.

I am struggling with a error flow finds and i am not able to solve it on my own. Its about using es6 classes and inheritance. More specific i created some react Components and they should inherit some methods.

I have a Callout Component, that represents a callout message of unspecified severity. To make things a little more simple i thought about providing a ErrorMessage Component, that inherits the Callout Component. My classes Structure looks like:

React.Component
    > AbstractComponent (here i add some project-wide helpers for i18n and so on
        > Callout (this represents a pretty message on the screen)
            > ErrorMessage (this represents an error)

Flow tells me:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/js/Components/Generic/ErrorMessage.js:14:43

statics of Callout [1] is not a polymorphic type.

     11│     icon: string
     12│ };
     13│
 [1] 14│ export default class ErrorMessage extends Callout<Props> {
     15│
     16│     static get defaultProps(): Props {
     17│         return {

The part Callout<Props> gets highlighted

I already define the Props Type for the Callout class, so this might be the problem but i cant solve it on my own.

A similar error is thrown a few lines below, where i try to access a parent method by addressing super.content (content is a get-method of Callout).

Thanks in advance

UPDATE: Why do i want to use class inheritance?

The inheritance Callout > ErrorMessage just exists to reduce redundant code, but its not necessary, so lets ignore this and talk about a more common case:

I want to have a class AbstractComponent to make common things in my project easier.

Some examples:

Printing of translation strings: In order to make the component multilingual, i created a utility class to generate translation strings, inside a component it works like

function render() {
    return (
        <div>
            {new Translation(
                'namespace',
                'key',
                [some, args],
                `${some} fallback message with optional ${args}`
            ).toString()}
        </div>
    )
}

In order to use this, every component in my stack ends up with the import statement on top

import Translation from "../Core/Translation"

or in the best case

import t from "../Core/Translation"

I use webpack to build a bundle and webpack seems to blow up the compiled javascript with every import statement you use. So i figured - to reduce coding effort and bundle size - i provide a intermediate component class, that adds some utility methods like:

class AbstractComponent extends React.Component {

    constructor(props) {
        super(props);
        this.logger = props.logger || new Logger();
        this.api: ApiInterface = props.api || new MockApi();
    }

    translate(namespace: string, key: string, args: ?[] = null, fallback: ?string): string {
        return new Translation(namespace, key, args, fallback).toString();
    }

    svgSprite(id: string, className: string = "") {
        return (
            <SvgSprite id={id} className={className} />
        )
    }

}

I also added some other things to show you more reason for a intermediate Component class.

So, all of this works! But flow complains about missing return types and so on, thats good with me, for that purpose i want to use flow! The problem i cant solve is the inheritance itself... But for me it does make a lot of sense.

like image 594
Philipp Wrann Avatar asked Mar 22 '18 15:03

Philipp Wrann


1 Answers

If you really want to deal with inheritance (which I don't have an issue with, I just feel like you will probably run into issues later), you can do something like the following:

class AbstractComponent<Props: {}, State: ?{} = null> extends React.Component<Props, State> {
    api: ApiInterface

    logger: typeof Logger

    constructor(props) {
        super(props);
        this.logger = props.logger || new Logger();
        this.api = props.api || new MockApi();
    }

    translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
        return new Translation(namespace, key, args, fallback).toString();
    }

    svgSprite(id: string, className: string = "") {
        return (
            <SvgSprite id={id} className={className} />
        )
    }
}

And use it like:

class Test extends AbstractComponent<{ some: string, args: string }> {
  render() {
    const { some, args } = this.props
     return (
        <div>
            {this.translate(
                'namespace',
                'key',
                [some, args],
                `${some} fallback message with optional ${args}`
            )}
        </div>
    )    
  }
}

Now, I will say that to some extent I understand where Facebook is coming from. Your component in this case is really already an abstract construct. And if you want this to be more flexible (let's say you have a stateless component that could benefit from having a logger and a translate function), you could do one of two things:

This is the defined type and translate function I'm using in both:

type CommonProps = {
  logger?: Logger,
  api?: ApiInterface,
  translate?: (namespace: string, key: string, args: ?string[], fallback: ?string) => string
}

// This should look familiar
function translate(namespace: string, key: string, args: ?string[] = null, fallback: ?string): string {
    return new Translation(namespace, key, args, fallback).toString();
}

Higher order component

function addCommonStuff({ logger = new Logger(), api = new MockApi(), translate = translate }: CommonProps) {
  return <Props: {}>(
    WrappedComponent: ComponentType<Props>
  ): ComponentType<
    $Diff<Props, $NonMaybeType<CommonProps>>
  > => (props: Props) => <WrappedComponent {...props} logger={logger} api={api} translate={translate} />
}

And used like:

class Test extends React.Component<{}> {}

const TestWithCommons = addCommonStuff({})(Test)

;<TestWithCommons /> 

Reusable component with a render prop

class Common extends React.Component<CommonProps & { render?: Function, children?: Function }, $NonMaybeType<CommonProps>> {
  state = {
    logger: this.props.logger || new Logger(),
    api: this.props.api || new MockApi(),
    translate: translate
  }

  render() {
    const { children, render } = this.props

    return typeof render === 'function' ? render(this.state) : (
      typeof children === 'function' ? children(this.state) : null
    )
  }
}

And use it like this:

class TestCommon extends React.Component<{}> {
   render() {
     return <Common>
       {({ logger, api, translate }) => translate('namespace',
        'key',
        null,
        `Fallback message`
      )}
    </Common>
   }
}

As an aside to this discussion, you don't need to write defaultProps as a getter for your callout. static defaultProps = {} should be enough. It shouldn't take passed in props into account or anything. If it does, you're better off using state

like image 146
kalley Avatar answered Sep 30 '22 17:09

kalley