Flow version: 0.112.0
Simple spread operation can be used to exclude properties from an object definition.
"exponentially large number of cases" error
I frequently use the following pattern to create objects with null/undefined properties omitted:
const whatever = {
something: true,
...(a ? { a } : null),
...(b ? { b } : null),
};
As of flow release v0.112, this leads to the error message:
Computing object literal [1] may lead to an exponentially large number of cases to reason about because conditional [2] and conditional [3] 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.
It sounds to me like this isn't really a type error, just that Flow is trying to avoid some heavier computation. This has led to dozens of flow errors in my project that I need to address somehow.
I asked on stack overflow for advice on a possible workaround. So far nothing I've tried to make the type system happy has worked.
In my opinion, this is a common js pattern that should work without any crazy type declarations, and therefore should be addressed as a bug.
How do you intend to use whatever? There are several ways to work around this error but they all depend on the type you expect the object to have.
If you open the try flow link provided you would see the expected type.
@jbrown215 please see the linked try flow from the op. The intended type is an object made up of some optional properties. They either exist with the given type, or do not exist.
type Built = {
something: boolean,
a?: A,
b?: B,
};
Code like this is often necessary, since unfortunately a property being set to undefined is not the same as not having that property. For example, Object.keys({a: undefined, b: 2}) returns ['a', 'b'].
Yeah I've run into this too. The way I'm working round it currently is by going off the last sentence of the error message:
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.
function buildObj(a?: ?A, b?: ?B): Built {
return {
...((a ? { a } : {}): { a?: A }), // <-- Give a general type to one of the unions
...(b ? { b } : null),
something: true,
};
}
Obviously not ideal as it's pretty verbose, but seems to get the job done. It's even more verbose if you're using exact objects:
function buildObj(a?: ?A, b?: ?B): Built {
return {
...((a ? { a } : { ...null }): {| a?: A |}),
...(b ? { b } : null),
something: true,
};
}
@jamesisaac the first suggestion doesn't work for me in practice, because as soon as you have more than 2, you run into the other new error:
Cannot determine a type for object literal [1]. object type [2] is inexact, so it may contain `userId` with a type that conflicts with `userId`'s definition in object type [3]. Can you make object type [2] exact?
The second suggestion with exact types does work, but it starts to get verbose to the point of being unreadable. Someone who is new to flow would have no idea what that code is doing. Especially with the { ...null }, because {} is incompatible with exact types...
Still hoping that this can be addressed.
We also use this pattern throughout our app and are now faced with many errors. Thanks @jamesisaac for the workaround while we wait for a fix 馃檹
The following workaround is an extra function call but it pushes out the complexity:
// function maybeSpread<T: {...}, U: $Exact<T>>(obj: U | null): $Rest<U | {||}, {...}> {
function maybeSpread<T: {...}>(obj: T | null): $ObjMap<T, <U>(U) => U | void> {
return obj === null ? {...null} : obj;
}
type Built = {|
something: boolean,
a?: boolean,
b?: boolean,
|};
function buildObj(a: boolean, b: boolean): Built {
return {
something: true,
...maybeSpread(a ? { a } : null),
...maybeSpread(b ? { b } : null),
};
}
I fell into this issue as well. I found the following work around with if statements:
const options: {|
canDelete?: boolean,
canEdit?: boolean,
|} = {
canDelete: true,
}
const moreOptions: {|
canDelete?: boolean,
canEdit?: boolean,
|} = {
...(options.canDelete && { canDelete: options.canDelete }),
};
if(options.canEdit) {
moreOptions.canEdit = options.canEdit;
}
I'm not a huge fan of such syntax though. It also force to explicitly type moreOptions to accept the new potential keys instead of infer them from object initialization.
Conditional spread creates optional properties in TypeScript 4.1 https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#conditional-spreads-create-optional-properties
Here's another very reasonable use case where I run into this issue:
type Overrides = {|
foo?: number,
bar?: string,
|}
type Options = {|
name: string,
anotherOption?: string,
overrides?: Overrides
|}
class Context {
options: Options
constructor(options: Options) {
this.options = options
}
clone(options: Options) {
return new Context({
...this.options,
...options,
overrides: {
...this.options.overrides, // Computing object literal [1] may lead to an exponentially large number of cases to reason about because inferred union from object literal [1] | `Overrides` [2] and inferred union from object literal [1] | `Overrides` [2] 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. [exponential-spread]
...options.overrides,
}
})
}
}
Most helpful comment
Yeah I've run into this too. The way I'm working round it currently is by going off the last sentence of the error message:
Obviously not ideal as it's pretty verbose, but seems to get the job done. It's even more verbose if you're using exact objects: