Photo by Erik Odiin on Unsplash

Spreads: Common Errors & Fixes

Jordan Brown
Flow
Published in
7 min readOct 30, 2019

--

In v0.111 the Flow team is rolling out a ton of fixes to object spreads (spreading an object in an expression, see “Spread in object literals”). These changes will expose a lot of errors in your codebases, so I recommend also reading Upgrading Flow Codebases in order to make upgrading easier. In this post, I’ll focus on the issues spread had before v0.111 and the behavior we have in those cases. Additionally, I’ll briefly touch on some of the new restrictions we’ve placed on objects in Flow in order to guarantee good worst-case performance for spreads.

Type spreads and value spreads now share the same underlying implementation, which is a major milestone in our larger goals around revamping our object model. You can read more about the design changes behind spreads and our object model in my post about fixing type spread.

Spreads now overwrite properties instead of inferring unions

Prior to v0.111, Flow did not take the order of spreads into account:

//@flowconst x = {foo: 3};
const y = {foo: 'string'};
// Flow imprecisely infers z.foo as having the type number | string.
const z = {...x, ...y};

[try-flow]

Flow now infers a type that more precisely matches the runtime behavior of spreads. Here, z is inferred to be { foo: string }.

Spreads no longer conflate unions into a single object

Flow inferred an incorrect type when spreading unions, creating a single object type. Instead, unions should distribute over spreads. The most common (and scary) way this manifests in code is via maybe types:

//@flowconst x: ?{| foo: number |} = null;
// No error. y is an empty object at runtime!
const y: {| foo: number |} = {...x};
// Flow should have inferred {| foo: number |} | {||}

[try-flow]

Now, we will error saying that foo is missing in the object type. This pattern is common in the calculateState function on Flux components:

//@flowtype State = {| foo: number, bar: number |};function calculateState(prevState: ?State): State {
const newState: State = {...prevState, foo: 3};
// This object may be missing bar at runtime
return newState;
}

[try-flow]

To fix these errors, you should check for null/undefined before spreading a maybe-typed object:

function calculateState(prevState: ?State): State {
if (prevState != null) {
return {...prevState, foo: 3};
}
const initialState = {foo: 3, bar: 3};
return initialState;
}

This behavior also made it confusing to spread disjoint unions. Since all of the branches would be collapsed into one, Flow would error saying it cannot figure out which case of the union to use:

//@flowtype DisjointUnion =
| {| type: 'A', payload: number |}
| {| type: 'B', payload: string |};
const disjoint: DisjointUnion = {type: 'A', payload: 3};
// Error, cannot choose which case to select
const disjointSpread: DisjointUnion = {...disjoint};

[try-flow]

Now you can spread disjoint unions without getting errors!

Spreads no longer assume that all optional properties exist

Flow used to unsoundly assume that all optional properties on the spreaded object were present:

//@flowconst x: {| foo: number, bar?: number |} = {foo: 3};
// No error, but y does not have bar at runtime!
const y: {| foo: number, bar: number |} = {...x};

[try-flow]

Now Flow errors saying that bar may be missing on x. There is no cookie cutter fix for this sort of error— you should audit your code to make sure that the properties will in fact exist at runtime.

Spreads no longer assume that all objects are exact

Flow did not take width-subtyping into account when inferring types for spreads:

//@flowconst a = {foo: 3};
const b: {...} = {foo: 'string'};
// No error, but foo is a string at runtime!
const c: {foo: number, ...} = {...a, ...b};

[try-flow]

Now Flow errors saying that b is inexact and may overwrite foo. To fix these errors, try making all your object types exact. If you can’t, you can try spreading the inexact object first instead:

const c: {foo: number} = {...b, ...a}; // Ok, and true at runtime

(Note that changing the order of the spreads will change the behavior of your code at runtime)

Spreads no longer become any when spreading an object with an indexer

When spreads included an indexer, Flow would silently give up and emit any:

//@flow
type X = { [string]: number };
const x: X = {foo: 3};
// No error, certainly not a boolean at runtime
const y: boolean = {...x};

[try-flow]

Now, Flow infers {[string]: number} for y in this example. Note that an indexer must come before any explicit keys, so new errors may mention indexers overwriting explicit keys.

Performant by Design

Inferring types for spreads can easily lead to extremely large union types, leading to very poor type checking performance. To avoid that, Flow will error if it encounters cases that may lead to massive union types. The details behind these restrictions will get their own blog post, but I’ll go over a few examples here.

Flow prevents exponential explosion induced through control flow

Spreading objects in the presence of control flow can cause Flow to produce giant unions:

// @flow
let obj1;
if (cond1) {
obj1 = {foo: 3};
} else if (cond2) {
obj1 = {bar: 3};
} else if (cond3) {
obj1 = {baz: 3};
} else {
obj1 = {qux: 3};
}
let obj2;if (cond1) {
obj2 = {foo2: 3};
} else if (cond2) {
obj2 = {bar2: 3};
} else if (cond3) {
obj2 = {baz2: 3};
} else {
obj2 = {qux2: 3};
}
const spread = {...obj1, ...obj2}; // 16 possible object types!

Instead of producing these massive unions and slowing down type checking performance, Flow will emit an error asking you to summarize the types of these objects:

Computing object literal [1] may lead to an exponentially large number of cases to reason about because inferred union
from object literal [2] | object literal [3] and inferred union from object literal [4] | object literal [5] are both
unions. Please use at most one union type per spread to simplify reasoning about the spread result. You may be able to
get rid of a union by specifying a more general type that captures all of the branches of the union.
[4] 5│ obj1 = {foo: 3};
:
[5] 11│ obj1 = {qux: 3};
:
[2] 17│ obj2 = {foo2: 3};
18│ } else if (cond2) {
[3] 19│ obj2 = {bar2: 3};
20│ } else if (cond3) {
21│ obj2 = {baz2: 3};
22│ } else {
23│ obj2 = {qux2: 3};
24│ }
25│
[1] 26│ const spread = {...obj1, ...obj2}; // 16 possible object types!
27│

In this case, these two types would work:

let obj1: {| foo?: number, bar?: number, baz?: number, qux?: number |};
let obj2: {| foo2?: number, bar2?: number, baz2?: number, qux2?: number |};

These errors are triggered when more than one object in the spread has a union type. This guarantees that the type Flow calculates is at most linear in the size of the largest union being spread.

Flow errors on unions in computed properties

Note: Computed properties {[someExpression]: 3} and object type indexers {[string]: number} have similar syntax, but represent very different things. Computed properties add some property to an object determined by its runtime value, and indexers are a Flow feature to express objects with keys and values conforming to certain types.

While computed properties are not actually spreads, their implementations are closely tied together. Naturally, while fixing spreads we also fixed issues with computed properties along the way.

Let’s take a look at what happens when you use a massive string literal union as a computed property:

type LargeUnion = 'member1' | 'member2' | ... | 'member10000';declare var computed_key: LargeUnion;
const x = {[computed_key]: 3};

Prior to v0.111, x would be typed as an object containing every string literal in the union as a key. This is clearly unsound, since at runtime this object only has one key.

The correct way to type x would be a union of objects, each containing a key from the massive union. In this example, that union would have 10k members. That means any time that you try to interact with x it’ll have to type check 10k possible objects. At best, this slows Flow down, and at worst you can actually cause so many errors that flow crashes.

Instead of inferring a 10k large object union, Flow will emit an error saying that you cannot use a union as a computed property.

To comply with this restriction, you should try to only use values with literal types as computed keys. If you can’t, you can annotate the object with optional properties and then explicitly set the property afterwards:

const x: 'a' | 'b' = 'a';const y: {a?: number, b?: number} = {};
y[x] = 3;

You can also use an indexer:

const x: 'a' | 'b' = 'a';const y: {['a' | 'b']: number} = {};
y[x] = 3;

Next Steps

While the vast majority of fixes are in, there’s still a couple things left:

  • spreads in jsx props still use the old spread semantics — this will be fixed soon
  • you can still spread unsealed objects, which can lead to unsound behavior — this will take longer to fix. In practice, the vast majority of these cases are just spreading the empty object literal.

That’s all!

Stay tuned for more blog posts that will drill down more into the interesting problems we had to solve to get this working!

--

--