String literal treated as regular string
Flow version: 0.157
Expected behavior
Refining a union type is difficult due to flow coercing a string literal into a string.
Note: This only occurs during a very specific case of using $Exact instead of {||} and spreading
type Union = $Exact<{
type: "ONE",
}> | $Exact<{
type: "TWO",
}>
type Props = $Exact<{
...Union,
}>
const func = (action: Props) => {
action.type === "TWO" && console.log(action.type)
}
^ Here "TWO" is treated as a string, which creates a flow error due to comparing string literal to string.
Actual behavior
No error
- Link to Try-Flow or Github repo: https://flow.org/try/#0C4TwDgpgBAqgdgSwPZygXigEgKIA8CGAxsADwDeAUFFKJAFxQBEA8gHLaMA0FAvgHxQAPljxFSlarQgNGAFQDqzLrz4UKUqAAUATkjABndCILFyVKADor8ZHG781hFPuBQAZgFc4hIwAoxtgw6evoAlOgCElABKBYaaAlMCkpQAGSpUE5w+kgANhAWuUgA5v7EtnHgEKG8QA
Good find, super easy to replicate too:
Flow error
type Union = $Exact<{
type: "ONE",
}> | $Exact<{
type: "TWO",
}>
type Props = $Exact<{
...Union,
}>
const func = (action: Props) => {
action.type === "TWO" && console.log(action.type)
}
No flow error
type Union = {|
type: "ONE",
|} | {|
type: "TWO",
|}
type Props = {|
...Union,
|}
const func = (action: Props) => {
action.type === "TWO" && console.log(action.type)
}
Yeah. It looks like key elements of triggering this issue are (a) $Exact, (b) on a union, (c) in a condition used for refinement.
On the other hand it's not specific to strings; it reproduces the same with numbers, or with two types like null and boolean instead of specific strings or specific numbers.
Here's a set of repro cases showing (a) and (b), and a variety of types (with v0.163.0) (try):
// Repro with `$Exact`, but not without:
type T = {| type: "ONE" |} | {| type: "TWO" |};
(a: $Exact<T>) => a.type === "TWO" && 0; // error, wrongly
(a: T ) => a.type === "TWO" && 0; // ok
// Doesn't repro without union:
type T2 = {| type: "TWO" |};
(a: $Exact<T2>) => a.type === "TWO" && 0; // ok
(a: T2 ) => a.type === "TWO" && 0; // ok
// Repro with the original types being inexact, same result:
type S = { type: "ONE", ... } | { type: "TWO", ... };
(a: $Exact<S>) => a.type === "TWO" && 0; // error, wrongly
(a: S ) => a.type === "TWO" && 0; // ok
// Repro with two different numbers instead of two different strings:
type R = { type: 1 } | { type: 2 };
(a: $Exact<R>) => a.type === 2 && 0; // error, wrongly
(a: R ) => a.type === 2 && 0; // ok
// Repro with two other types, not numbers or strings:
type Q = { type: null } | { type: boolean };
(a: $Exact<Q>) => a.type === false && 0; // error, wrongly
(a: Q ) => a.type === false && 0; // ok
Here's a set of repro cases indicating (c) (try):
type T = {| type: "ONE" |} | {| type: "TWO" |};
type ET = $Exact<T>;
(a: ET) => {
// No repro just making the comparison, even with type annotation.
a.type === "TWO"; // ok
(a.type === "TWO": boolean); // ok
// Repro with && and ||…
a.type === "TWO" && 0; // error, wrongly
a.type === "TWO" || 0; // error, wrongly
// … but not ??.
a.type === "TWO" ?? 0; // ok
// Repro as condition in various control flow…
if (a.type === "TWO"); // error, wrongly
while (a.type === "TWO"); // error, wrongly
do; while (a.type === "TWO"); // error, wrongly
// … including the test in `for`…
for (;a.type === "TWO";); // error, wrongly
// … but not the other parts of `for`.
for (;;a.type === "TWO"); // ok
for (a.type === "TWO";;); // ok
// Repro even in for..of and for..in RHS expressions…
// which, as it happens, are used for refinements
// (predicates_of_condition gets called).
let i;
for (i of (a.type === "TWO")); // same "Cannot compare" error as above
// (in addition to the sensible "`@@iterator` missing" error)
for (i in (a.type === "TWO")); // same "Cannot compare" error as above
// (in addition to the sensible "Cannot iterate" error)
}
The failing to repro on ??, when it does on && and ||, seems particularly like a clue that refinement is involved. That's because while those three operators are in principle very similar in structure, it happens that Flow does no refinement on ?? while it does on the other two.
@charltongroves It'd be good to update the issue title to mention $Exact, unions, and/or refinement, rather than strings. That will help distinguish it from other issues, as those are elements that seem likely to be involved in the code where the bug is.