flow icon indicating copy to clipboard operation
flow copied to clipboard

Flow infers inconsistent types on mutable array

Open kylehg opened this issue 5 years ago • 0 comments

Flow version: v0.108.0 [latest]

I think I've found a place where Flow fails to preserve the invariance of types in JS arrays, by letting an array check as two incompatible types at once.

Consider a simple type hierarchy:

class Named {
  name: string
  constructor(name: string) { this.name = name }
  getName(): string { return this.name }
}
class Human extends Named {
  getFirstName(): string { return this.name.split(' ')[0] }
}
class Dog extends Named {}

And some functions which take and mutate those types:

function getFirstNames(hs: Human[]): string[] { return hs.map(h => h.getFirstName()) }
function mutateNamedArray(ns: Named[]) {
  ns.push(new Dog('Ghost'))
}

Expected behavior

If I create an explicit array of Humans, Flow correctly doesn't let me pass that to a function which takes an array of Nameds, since JS arrays are invariant on their element type:

const humans: Human[] = [new Human('Jon Snow'), new Human('Sansa Stark')]
mutateNamedArray(humans) // $ExpectError
getFirstNames(humans)

✅ This works (i.e., fails typechecking) as expected (link)

Actual behavior

But if I remove the type hint on humans, Flow seems to alternately infer humans: Human[] or humans: Named[], even though this is unsound:

const humans = [new Human('Jon Snow'), new Human('Sansa Stark')]
mutateNamedArray(humans)
getFirstNames(humans) // This will throw a TypeError: h.getFirstName is not a function

❌ This will throw an actual TypeError I run it, but Flow considers it safe (link)

It makes sense that Flow would let you pass humans as Named[] if you didn't provide a hint the first time to mutateNamedArray, but I would expect it to then force humans to be typed as Named[] for the rest of the array (or at least the variable's) lifecycle, and error when attempting to call getFirstNames()

Full example at TryFlow

kylehg avatar Oct 01 '19 17:10 kylehg