Photo by Jason Yu on Unsplash

New Flow Errors on Unknown Property Access in Conditionals

TL;DR: Starting in 0.68.0, Flow will now error when you access unknown properties in conditionals.

Flow already disallows accessing unknown properties outside of conditionals

Let’s say I want to write a function that takes a person object and returns the name of that person’s dog. If that person doesn’t have a dog, it will return undefined:

function getNameOfDog(person: { age: number }): void | string {
return person.nameOfDog;
}

If the object person doesn’t have a property nameOfDog, then the expression person.nameOfDog will evaluate to undefined. So this code does work as expected. However, Flow 0.67.1 will produce the following error.

2:   return person.nameOfDog;
^ Cannot get `person.nameOfDog` because property `nameOfDog` is missing in object type [1].
References:
1: function getNameOfDog(person: { age: number }): void | string {
^ [1]

We require our users to annotate what properties might exist. The following would produce no Flow errors:

function getNameOfDog(
person: { age: number, nameOfDog?: string },
): void | string {
return person.nameOfDog;
}

Why does Flow disallow this valid JavaScript pattern? It is because we found that spelling mistakes in property names are a very common source of bugs in JavaScript. By banning accessing unknown properties, we can catch errors like this:

function getNameOfDog(
person: { age: number, nameOfDog?: string },
): void | string {
return person.nameofdog; // Wrong capitalization!
}

Existence testing in conditionals

However, until 0.68.0, Flow would allow you to test the existence of unknown properties in conditionals. So Flow would allow you to write

function reportDogOwnerToPolice(person: { age: number }): void {
if (person.hasDog) { callPolice(); }
}

and if you accidentally wrote

if (person.hasDoge) { callPolice(); }

Flow would miss your spelling mistake.

If you tested whether an unknown property existed, Flow would refine that unknown property to mixed. So you could write

function getNameOfDog(person: { age: number }): mixed {
return person.dog && person.dog.name ? person.dog.name : null;
}

and Flow would infer that,

  • if person.dog exists, it has the type mixed.
  • If person.dog.name exists, it has the type mixed.

But again, Flow would fail to point out spelling mistakes. And furthermore, Flow would allow code that was trivially incorrect, like accessing imaginary properties on Number.prototype!

function abs(x: number): number {
return x.isNegative ? x * -1 : x;
}

We‘ve changed this behavior!

Now accessing unknown properties in conditionals behaves like accessing them outside of conditionals.

function abs(x: number): number {
return x.isNegative ? x * -1 : x;
}

produces the error

2:   return x.isNegative ? x * -1 : x;
^ property `isNegative` is missing in number [1].
References:
1: function abs(x: number): number {
^ [1]

and

function getNameOfDog(person: { age: number }): mixed {
return person.dog && person.dog.name ? person.dog.name : null;
}

produces the error

2:   return person.dog && person.dog.name ? person.dog.name : null;
^ property `dog` is missing in object type [1].
References:
1: function getNameOfDog(person: { age: number }): mixed {
^ [1]

To fix that second example to work with Flow 0.68.0, I would change it to

function getNameOfDog(
person: { age: number, dog?: { name?: string} },
): string | null {
return person.dog && person.dog.name ? person.dog.name : null;
}

Union types

You can test the existence of a property which exists in some branch of the union inside the conditional. With Flow 0.68.0, the following code

function nameOfPet(
pet: {|catName: string|} | {|dogName: string|}
): string | void {
if (pet.catName) { return pet.catName; }
if (pet.dogName) { return pet.dogName; }
if (pet.zebraName) { return pet.zebraName; }
}

produces the error

6:   if (pet.zebraName) { return pet.zebraName; }
^ property `zebraName` is missing in object type [1].
References:
2: pet: {|catName: string|} | {|dogName: string|}
^ [1]

Accessing catName and dogName are allowed, because they each appear in the union. But accessing zebraName is disallowed because it doesn’t appear anywhere.

Classes

Before Flow 0.68.0, the following would have produced no errors

class Animal {}
class Dog extends Animal { name: string }
class UnnameableBeast extends Animal {}
function nameOfPet(pet: Animal): mixed {
if (pet.name) { return pet.name; }
}

But in Flow 0.68.0, you’ll get the error

6:   if (pet.name) { return pet.name; }
^ property `name` is missing in `Animal` [1].
References:
5: function nameOfPet(pet: Animal): mixed {
^ [1]

The fix is to either declare name in Animal.

class Animal { +name: string | void }
class Dog extends Animal { name: string }
class UnnameableBeast extends Animal {}
function nameOfPet(pet: Animal): mixed {
if (pet.name) { return pet.name; }
}

Or to use instanceof

class Animal {}
class Dog extends Animal { name: string }
class UnnameableBeast extends Animal {}
function nameOfPet(pet: Animal): mixed {
if (pet instanceof Dog) { return pet.name; }
}

Feedback welcome!

We made this change after receiving a lot of feedback about Flow missing spelling mistakes in property names. Love this new behavior? Hate this new behavior? Let us know!