Improved handling of the empty object in Flow
Flow handled the empty object literal {}
in a permissive but unsafe way. The fix increases safety and predictability, but requires using different patterns and behavior described in this post.
Flow treated the empty object literal {}
as an object which could change over time as values were written to and read from it (we called this “unsealed”). This was purposely done when Flow first started to be adopted, in order to support the existing pattern of multi-line object initialization. However, this also lead to a lot of unsafe behavior and user frustration.
We have fixed these issues, but now require some new patterns to be used. This change not only improves safety, but also unblocks Flow’s move to local type inference.
The Problems
1) You could read non-existent properties from the object, and this access would result in any
:
const obj = {};
const n: number = obj.XXX; // Previously allowed, now an error
2) You could use an empty object where an object with properties was expected:
const obj: {prop: boolean, ...} = {}; // Previously allowed, now an error
3) The empty object was considered “inexact”, so was not a valid value for an exact object with optional properties:
declare function f({|prop?: boolean|}): void; f({}); // An error previously, now ok
const obj: {||} = {}; // Also an error previously, now ok
4) Spreads involving the empty object were also unsafe:
const obj = {};
function f() { obj.foo = 3; } // Function never called
let y: {foo: number} = {...obj}; // Previously allowed
The Solutions
Now the empty object literal is treated as an empty exact object (with no properties), resolving the above issues.
const obj: {||} = {}; // Now ok!
obj.XXX; // Error: property 'XXX' doesn't exist on 'obj'
To support this, we made changes to both Flow itself and have some new suggested patterns to use in your code.
To enable:
- For Flow versions 0.184–0.190, the behavior is off by default, but can be enabled by adding
exact_empty_objects=true
to your.flowconfig
- The option is enabled by default in Flow version 0.191
- The
.flowconfig
flag was deleted in Flow version 0.192, and behavior is enabled permanently
We have created various codemods to assist you in your migration. You must be using Flow version 0.184–0.191 to run the codemods, as they may be deleted when the flag was deleted (0.192). They are mentioned under the subheadings below. Note that given the previous unsafe behavior of the empty object, even after running codemods it is normal to have many Flow errors which will require $FlowFixMe
comments to suppress — more legitimately unsafe behavior is now being caught.
Object Access
We changed how we treat object access in Flow. Consider the following pattern:
const maybeObj: ?{a: number} = ...;
const obj = maybeObj ?? {};
obj.a;
Previously, this only worked because we unsafely allowed arbitrary reads from the unsealed empty object. If we treated the empty object as exact, then we would get an error cannot read property 'a' from '{}'
.
We modified how object access is handled: now if you access a property on a union (from the above {a: number} | {}
), and the access succeeds in one case (e.g. from the {a: number}
), rather than erroring for the other cases (e.g. {}
), if the other cases are exact allow it and add void
to the resulting type (in our example, obj.a
would have the type number | void
). This mirrors what happens at runtime, and matches users expectations.
If one of the members of the union is inexact, and does not have the property, we still error as an inexact object could have that property with some arbitrary type (so we cannot just add void
).
This behavior has been enabled since version 0.174.
Object initialization
Instead of initializing an object over multiple lines, either create it all at once in one literal or use an annotation. Consider
const obj = {};
obj.a = 1;
obj.b = 2;
This will now cause errors as obj
is not typed as having properties a
and b
. Instead you should create the object literal all at once:
const obj = {a: 1, b: 2};
A similar case to the above, say you have
const obj = {};
if (cond) { obj.a = 1; }
You cannot collapse this into an object literal. However, you can add an annotation to obj
with optional properties:
const obj: {a?: number} = {};
if (cond) { obj.a = 1; }
Codemod: Get the latest flow-upgrade package and then runyarn run flow-codemod collapseObjectInitialization
Dictionaries
Objects used as dictionaries require annotations. Consider
const dict = {};
for (const x of arr) { dict[x] = f(x); }
We are treating the object like a dictionary, with the empty object being the empty dictionary. You need to add an annotation to type it as a dictionary:
const dict: {[string]: number} = {};
for (const x of arr) { dict[x] = f(x); }
Using object dictionaries with .reduce
is a common pattern - you can supply the type argument to .reduce
to give the type of the empty object initializer:
arr.reduce<{[string]: number}>((acc, x) => acc[x] = f(x), {});
Codemod: flow codemod annotate-empty-object --write
Object.assign
Instead of using Object.assign
on an empty object, use object spread instead. Consider
const obj = Object.assign({}, a, b);
Flow requires the “target” of Object.assign
(the first parameter), to have the properties that that latter arguments will assign to. If the first argument of Object.assign
is an object literal (as in the case of {}
), you can transform it to an object spread instead:
const obj = {...a, ...b};
Use object spread instead of Object.assign
for the above pattern.
Codemod: Enable the existing ESLint rule prefer-object-spread as an error and run its autofixer on your codebase, e.g. eslint --rule '{"prefer-object-spread":2}' --fix
useState
Supply a type parameter if calling useState
with an empty object initial value. Consider
const [foo, setFoo] = useState({});
You can’t access any properties from foo
, as it doesn’t have any. You can solve this by supplying the explicit type argument:
const [foo, setFoo] = useState<{a?: number}>({});
Codemod: flow codemod annotate-use-state --write