Flow version: master, v0.92.1
/* @flow */
type X = { a: string };
let obj: X = {};
let str: string = obj.a;
Behaves as expected in Flow v0.88.0:
3: let obj: X = {};
^ Cannot assign object literal to `obj` because property `a` is missing in object literal [1] but exists in `X` [2].
References:
3: let obj: X = {};
^ [1]
3: let obj: X = {};
^ [2]
No error in Flow v0.89.0 and newer.
This is intentional. The empty object {} is treated as unsealed. If you want to prevent this behavior, mark X as exact as so:
type X = {| a: string |};
let obj: X = {};
let str: string = obj.a;
Still, within a declaration-with-initializer statement, Flow should be able to know that it doesnāt satisfy the requirements of the destination type, right?
This is an unfortunately side-effect of the way that unsealed objects behave. If you want to avoid this behavior, I strongly recommend not using them, and making all your object types exact in order to get the guarantees that you want.
That makes sense, but even if it's intentional that {} is treated as unsealed, I would still consider this a pretty dangerous behavior. Here's another example: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoALgTwA4FMwAaYAvGAN5gCGAXGAM7oBOAlgHYDmYAvgNyqpQArqwDG6ZnFZh2udAEEAFHABGAK1oEAlOVRgwjWYMZSVqgHSU+XfjFn0mtBiw4lpsxWS6a+QA
type X = { a: string };
function getA(obj: X) {
return obj.a; // looks good, any object of type X should have the .a property
}
// elsewhere...
let str: string = getA({}); // no error
The recommendation to use exact object types makes sense, and I generally try do to that, but this behavior seems to show that someone using a non-exact type can't even rely on the properties they expected being present at all š Isn't this a basic failure of soundness?
I noticed the docs about unsealed objects mention:
reads from unsealed objects with no matching writes are never checked. This is an unsafe behavior of Flow which may be improved in the future.
So is this bug a duplicate of an existing bug, or is there a ticket tracking this somewhere that I can follow?
It's certainly unsafe, but it is sound up to undefined. Most languages only make this guarantee; consider array accesses as an example. Most languages don't require you to bounds check all of your accesses manually, they just assume that you are accessing a value that is actually present in the array. This is the same principle. We have been debating what to do about unsealed objects, but have yet to reach consensus on the right way to proceed.
Can you explain more about what you mean by soundness āup to undefinedā? Is it to say that any value in the program might have a value of the type it claims to be, or it might be undefined? Iām not very familiar with the precise guarantees Flow is trying to make, but it seems like preventing proliferation of undefined where itās not expected is one of the major features the type system provides...
Well in a gradually typed language like Flow (or Typescript or Hack) any type might have the type it claims to be, or it might have any other type, because of the existence of any. Consider:
var x : string = "hello"
var y : any = 4;
x = y;
But if we put that aside for the moment, the semantics of Javascript make it very difficult to type object/array accesses in a way that is both sound and easy to use. The sound way to type class, object or array access is to have the access return a union of T and undefined, but this would require programmers to explicitly check all of their accesses and we (along with most other languages) made a usability decision not to require this. If it is important to you that your object accesses not return undefined, the solution is to use exact types. Similarly, if you need to be sure that an array access is not undefined, you can check it explicitly or use a tuple type which has a known size.
Thanks for the detailed explanation.
I still think there is a UX bug here. Sorry for my imprecise language but I'll try to explain: even though {} is treated as unsealed, I think the unsealed-ness should only extend as far as types are still being inferred. In my example with function getA(obj: X), getA({}) should be an error because the unsealed object was not modified/accessed at all before it was passed to a parameter of a specific/sealed type. (Note that getA(({}: {})) is in fact an error.)
Why was this significant change not mentioned in the changelog? This leads to a lot of surprising changes in behavior.
Most helpful comment
Why was this significant change not mentioned in the changelog? This leads to a lot of surprising changes in behavior.