Flow and Object.assign

13 March 2016

Flow has some helpful behavior for object literals:

/* @flow */
type T = {x: number};
const o = {};
o.x = 1;
(o : T);

Without the o.x = 1; line, the cast to T will fail:

src/flow-test.js:5
  5: (o : T);
          ^ property `x`. Property not found in
  5: (o : T);
      ^ object literal

Because the object literal is empty, it is “unsealed”. If it was created as const o = {y: 2};, Flow would fail on o.x = 1;

src/flow-test.js:4
  4: o.x = 1;
       ^ property `x`. Property not found in
  4: o.x = 1;
     ^ object literal

This same Flow behavior comes into play with Object.assign().

/* @flow */
Object.assign({x: 1}, {y: 2});

{x: 1} is sealed, so the property y can’t be added.

src/flow-test.js:2
  2: Object.assign({x: 1}, {y: 2});
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ property `y` of object literal. Property not found in
  2: Object.assign({x: 1}, {y: 2});
                   ^^^^^^ object literal

Object.assign({}, {x: 1}, {y: 2}); will work, because {} is unsealed. Flow knows the shape of the result. Running flow suggest:

src/flow-test.js
--- old
+++ new
@@ -1,2 +1,2 @@
 /* @flow */
-const o = Object.assign({}, {x: 1}, {y: 2});
+const o: {x: number, y: number} = Object.assign({}, {x: 1}, {y: 2});

o is unsealed, which I don’t think you can tell using type-at-pos. This has some unfortunate consequences in a less contrived example:

/* @flow */
type T1 = {x : number};
type T2 = {y : number};
type T3 = T1 & T2;

function merge(t1 : T1, t2 : T2) : T3 {
  const result = Object.assign({}, t1, t2);
  console.log(result.info.toString()); // at one time, T1 contained info, but it was removed
  return result;
}

This typechecks, but because result is unsealed, Flow won’t prevent the access of the possibly undefined property result.info. Flow still suggests the {x: number, y: number} type, even though this would cause the code not to typecheck.

I’m impressed by the inferences Flow is able to make, and I’d like it to be stricter about what it allows. There are very few locations where I’d make use of unsealed objects, so I’d like them to be optional–either globally, set in .flowconfig, or as a type (UnsealedObject instead of Object?).