Why does Flow allow mismatched type comparisons when one operand is an array access?

167 views Asked by At

The two comparisons below seem equivalent, but only one gets a type error (playground):

function test(arr: string[]) {
  const el = arr[0];
  if (el === 2) { }                 // Error here
  if (arr[0] === 2) { }             // No error here
}

What's the motivation behind this behavior?

1

There are 1 answers

0
user3840170 On

Hard to say, really. But Flow actually allows most mismatched-type comparisons.

As of 0.218.1, Flow will warn about none of the following:

declare let foo: string;
declare let bar: number;
declare function ident<T>(x: T): T;
declare function assert(x: boolean): void;

if (foo === bar) {}
if ("foo" === 42) {}
if (ident(foo) === 42) {}
if (foo === ident(42)) {}
if (foo === (0, 42)) {}
if ((0, foo) === 42) {}
if ((0, (foo === 42))) {}

switch (foo) {
case (0, 42): ;
case ident(42): ;
}

switch (0, foo) {
case 42: ;
}

switch (0, 42) {
case foo: ;
}

assert(foo === 42);

You may notice that Flow will warn about the examples above if the === operator is replaced with ==. From this you may surmise that in general Flow considers === an explicit check of both type and value: if a comparison is done between different types, so what? It’s just a comparison that returns false, it’s not like it throws a TypeError or anything.

So maybe you should not be asking why Flow allows identity comparisons between mismatching types when one comparand is an array indexing expression. That’s the default case. You should be asking why it disallows them exactly when the following conditions are true:

  • the comparison expression is (a logical conjunct of) the controlling expression of a conditional branch (if, while, or do-while), or the comparison is performed as part of a switch statement
  • one of the comparands is a variable or a property access on a variable
  • the other comparand is a literal

You may notice that switch statements always result in a strict comparison, so if those didn’t warn, switching on an enumeration type (Flow’s native enumerations, or a union-of-literals type) would effectively be left unchecked unless you wrote them out as a stack of ifs. You wouldn’t be warned if you accidentally checked a value against an enumerator it can’t ever assume:

declare let quux: "foo" | "bar";

switch (quux) {
case "foo":
  // ...
case "baz":        // oops, butterfingers
  // ...
}

You may also notice that unlike general Boolean expressions, comparisons controlling a branch statement subject the non-literal comparand to type refinement: the type of the variable is narrowed down in both the successful and unsuccessful branches to match the result of the test. By a similar token as with the switch, if a type of a variable (which wasn’t the bottom type to begin with) is narrowed out of inhabitants, this may be an error in the code. Except that array accesses are just as much subject to refinement, yet they don’t trigger errors on mismatched-type comparisons:

declare const arr: string[];
declare const i: number;

if (arr[i] === void 0) {
  (arr[i]: empty);         // …unreachable?!
}

You may be tempted to think that Flow wants to sanction testing arr[i] against undefined to check if an array contains the i-th element. But the type of arr[i] above is not merely narrowed to void (the type of undefined); it’s narrowed to empty, the bottom type with no values. Having a value of the bottom type in scope is an impossibility, and is supposed to mean the code is unreachable, and therefore can be removed. So Flow seems to assume indexing arrays always results in a present element, for otherwise this would be a soundness hole.

You may further ask ‘But wouldn’t a mismatched-type comparison be just as much indicative of an error in the general case of an arbitrary expression? What makes refinements of a variable so special?’ Well, yes, it would. Because of that, I have reason to suspect which mismatched-type comparisons trigger errors and which don’t was not deliberately chosen by some general principle, but is an emergent outcome of certain more specific, ad-hoc checks. And I can only speak of a ‘reason to suspect’ because none of this seems to be documented anywhere in Flow documentation (and I looked pretty thoroughly); every rationale above is my conjecture. For all I know, this wasn’t specifically decided, and array-element refinements may have just slipped through. Deal with it.