flow icon indicating copy to clipboard operation
flow copied to clipboard

String literal treated as regular string

Open charltongroves opened this issue 4 years ago • 2 comments

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

charltongroves avatar Aug 09 '21 01:08 charltongroves

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)
}

mitchbne avatar Aug 09 '21 01:08 mitchbne

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.

gnprice avatar Nov 05 '21 07:11 gnprice