I am currently studying structural typing. I am skeptical about the idea that two types are considered equivalent just because they happen to have a portion of their structure in common. This feels a lot like static duck typing and it completely ignores the semantic level of types. So I took a closer look at flow's structural typing of ordinary objects and have encountered the following behavior:
const o:{} = {foo: true};
o.foo; // type error
{}
is a structural type and the supertype of all ordinary objects. Hence it makes sense that I can annotate o
with it, because {foo: true}
is a structural subtype of {}
. However, when I try to access the existing foo
property, this operation doesn't type check. This is odd because AFAIK a structural subtype can usually contain specific properties as long as it also includes all required properties of its supertype.
It seems as flow's structural subtyping algorithm occasionally forgets properties that are specific to a certain subtype. Is this behavior intended or did I just run into an edge case?
The overall issue you're describing is a fact of casting from a subtype to a supertype. By performing the cast, you're explicitly telling the compiler to discard information about a given object.
For instance, even without structural typing, if you do
(On flow.org/try)
it fails to typecheck for the same reason. In your example you've explicitly told the compiler "consider
o
to have the type{}
", just like in my example I've said "considerc
to have the typeAnimal
". Because of that, the compiler has explicitly been told to forget that it is working with aCat
, therefore it forgets that the object has a.foo
property.So to answer this, it doesn't do it "occasionally", it does it exactly when you tell it to do it. The behavior is absolutely intended.
The object does contain that property, which is 100% fine, but you've explicitly erased that information by casting the value to a supertype.