Flow: Static property issue

Created on 1 Sep 2017  路  3Comments  路  Source: facebook/flow

Flow does not seem to like having static properties with the same name.

class One {
    static foo = {one: 1}
}

class Two extends One {
    static foo = {two: 2}

    doStuff() {
        alert(One.foo.one + ' ' + Two.foo.two)
    }
}

const two = new Two()

two.doStuff()

The code works as expected but I get the following a flow error

     alert(One.foo.one + ' ' + Two.foo.two)
              ^ property `one`. Property not found in
    alert(One.foo.one + ' ' + Two.foo.two)
             ^ object literal
    alert(One.foo.one + ' ' + Two.foo.two)
                                ^ property `two`. Property not found in
    alert(One.foo.one + ' ' + Two.foo.two)
                                ^ object literal

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjGBDAzrsAeQDsBTMAb1TBrFwBdt6BLDMKOOMAXkrjIBcYAIwBfVOMw58YACoIupAB71SxACYES5KrTqMWbDl14V6CoQCZJe9XADK9AK5QoACgCUlantrYYpABO9G7aAHTGYfzkANRgAOQJYHHycBGcYeZwHj404pIY-AxgWTxgZEipnuhZYXaOLu4eQA
screen shot 2017-09-01 at 15 58 47

Babel seems happy with it.
https://babeljs.io/repl/#?babili=false&browsers=&build=&builtIns=false&circleciRepo=&code_lz=MYGwhgzhAEDyB2BTaBvAUNT0IBcw4EthoAzAezOgF5UykAuaARgF8021RIYAVAd0qIAHjkTwAJjATJ0WbHkLFylGihwDGAJg5zxZAMo4AriRIAKAJSoMcrGBCIATjjPSAdMrd1kAamgByAOg_fjIPCjd1MgsbTDYOYDpcaCjqaCQ-aFDLNDQotz1DE3MLIA&debug=false&evaluate=true&lineWrap=false&presets=stage-2&prettier=false&showSidebar=true&targets=&version=6.26.0
screen shot 2017-09-01 at 16 13 22

Most helpful comment

Thanks for your response @asolove

I've been trying to work out what Flow is trying to do here, as you say it seems flow wants the types to be identical, I can't see any reason why that has to be true (Is this part of some specification i'm not aware of?)

This is valid javascript as far as i'm aware.

class One {
    static foo: number = 0
}

class Two extends One {
    static foo: string = 'FOO'
}

alert(Two.foo.toLowerCase()) // foo

But flow does not like it.

4:     static foo: number = 0
                   ^ number. This type is incompatible with
8:     static foo: string = 'FOO'
                   ^ string
8:     static foo: string = 'FOO'
                   ^ string. This type is incompatible with
4:     static foo: number = 0
                   ^ number

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjGBDAzrsAeQDsBTMAb1TBrFwBdt6BLDMKOOALjGIFcAtgCNSAJzABeMAAZUAX3RY8BACoI4YUgA96pYgBMCJclVp1GLNh27nRzYgHNJYAOQAxQoRfz02GGPoACjU4ADprUPo4ABlEMQBhPFJAgEoUsGBgdk4gA

All 3 comments

I think the issue here is that since Two extends One, their static properties are expected to be of the same type. Because of the initial values in each, Flow is inferring a type that I think is like {a: number} | {b: number}. For that type, these errors are valid.

If you want foo to just be treated as a dictionary with any string key pointing to a number, that seems to work by annotating the property as such. There are no type errors if I do that:

/* @flow */

class One {
    static foo: {[key: string]: number} = {one: 1}
}

class Two extends One {
    static foo = {two: 2}

    doStuff() {
        alert(One.foo.one + ' ' + Two.foo.two)
    }
}

const two = new Two()

two.doStuff()

Is that what you want the type of this to be? If not, and you can share more about the contact where you do this, we can probably find another solution.

Thanks for your response @asolove

I've been trying to work out what Flow is trying to do here, as you say it seems flow wants the types to be identical, I can't see any reason why that has to be true (Is this part of some specification i'm not aware of?)

This is valid javascript as far as i'm aware.

class One {
    static foo: number = 0
}

class Two extends One {
    static foo: string = 'FOO'
}

alert(Two.foo.toLowerCase()) // foo

But flow does not like it.

4:     static foo: number = 0
                   ^ number. This type is incompatible with
8:     static foo: string = 'FOO'
                   ^ string
8:     static foo: string = 'FOO'
                   ^ string. This type is incompatible with
4:     static foo: number = 0
                   ^ number

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjGBDAzrsAeQDsBTMAb1TBrFwBdt6BLDMKOOALjGIFcAtgCNSAJzABeMAAZUAX3RY8BACoI4YUgA96pYgBMCJclVp1GLNh27nRzYgHNJYAOQAxQoRfz02GGPoACjU4ADprUPo4ABlEMQBhPFJAgEoUsGBgdk4gA

Your example code is unsound.

The root of this evil is ES6's class inheritance versus the prototype chaining typical of ES5 "classes". Consider

function Base() {}
Base.prototype.m = function m() {}
Base.aStatic = 1;

function Subclass() {}
Subclass.prototype = Object.create(Base.prototype);

//Subclass.__proto__ = Base; // Uncomment this to get ES6's class behavior.

Notice that aStatic isn't accessible from Subclass, i.e. Subclass.aStatic is undefined.

If you implement the preceding as ES6 classes and transform them to ES5 with Babel, you'll notice that the code contains something like the Subclass.__proto__ = Base. ES6 classes, fortunately or unfortunately, expose base class statics on subclasses, so the ES6 version will evaluate Subclass.aStatic as 1. (Node exposes a utility function that does class inheritance for ES5 "classes": https://nodejs.org/api/util.html#util_util_inherits_constructor_superconstructor. The docs say its use is discouraged, but it isn't marked for deprecation.)

To demonstrate the unsoundness of your sought behavior, suppose I've got an interface, Fooy:

interface Fooy {
  foo: number;
}

Your code would introduce a contradiction:

(One: Fooy);      // This is okay because value `One` has type `Class<One>`
                  // and `Class<One>` has a `foo: number` property.

(Two: Class<One>) // This is okay because value `Two` has type `Class<Two>`
                  // and if `Class<Two> is a subclass of `Class<One>,
                  // then `Class<Two>` subtypes `Class<One>`.

(Two: Fooy):      // This is no good and okay at the same time. The no good
                  // case is obvious: `Class<Two>` doesn't have a
                  // `foo: number` property. For the okay I gotta introduce
                  // a transitive property: If A subtypes B and B subtypes
                  // C, then A subtypes C. Since `Class<Two>` subtypes
                  // `Class<One>` and `Class<One>` subtypes `Fooy`, then
                  // `Class<Two>` subtypes `Fooy`. Since value `Two` has
                  // type `Class<Two>`, the cast is okay.

There's no way that the transitive property is going away, although I recall Flow rejecting (Two: Class<One>) in the past. In that universe your use case could pass. I'm pretty sure that TypeScript admits your use case by not exposing base class statics on subclasses (I prefer Flow's behavior).

You might find the following useful:

class One {
    static +foo: number | string = 0;
}

class Two extends One {
    static +foo: string = 'FOO'
}
Was this page helpful?
0 / 5 - 0 ratings