Improved handling of the empty object in Flow

George Zahariev
Flow
Published in
4 min readOct 20, 2022

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 .reduceto 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

--

--