flow
flow copied to clipboard
Flow infers inconsistent types on mutable array
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 Human
s, Flow correctly doesn't let me pass that to a function which takes an array of Named
s, 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()