Flow: Passing input without optional properties to fn throws error

Created on 31 Jul 2017  路  10Comments  路  Source: facebook/flow

/* @flow */

type O = {
  str: string,
  str2?: string,
  str3?: string,
};

type A = {
  str: string,
};

const a: A = { str: '' };

function fn(o: O) {
 if (typeof o.str2 === 'string') {
   console.log('ay');
 }
}

fn(a);

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgDyYAvGAN6phgDOGATgFy0MCWAdgOYA0VY7AVwC2zQUIBGeerwC+AbnTZ8YAApwaNVuJh4V9ODhqkKfHPpwBGZnXocep8wCZrbLrIWZcBQhgAWUvQMjMkpqMwMAZhdbNwcDABZou3dFLzAAFWNiADIwAAo1DS0dQMMwAB8iPwDzGgBKDygBdgBjDFY4djAodjy4ZnS6k2pWKHy4ADpwyyHQ6jB6PAwBei7J6ccFahlUGSA

Shouldn't this typecheck correctly ?
The logic is this code can't crash ; A fits B actually, as str2 and str3 are optional properties.

Yet flow throws theses errors:

15: function fn(o: O) {
                   ^ property `str2`. Property not found in
21: fn(a);
       ^ object type
15: function fn(o: O) {
                   ^ property `str3`. Property not found in
21: fn(a);
       ^ object type

Is this logical for Flow to throw an error and why ?
If not, how can I make this typecheck correctly ?

All 10 comments

This typechecks correctly, you make the assumption that A is a subtype of O (which seems logical), however, nothing in your types says that A doesn't have an str2 prop. It could actually even be a number. What you're looking for here are exact objects, see your corrected example.

I'm sorry for not explaining well this case, I hope that the docs will do a better job than me!

This exemple may explain why it's logical that flow throw an error :

/* @flow */

type A = {
  str: string,
  str2?: string,
  str3?: string,
};

type B = {
  str: string,
}

type C = {
  str: string,
  str2?: number,
  str3?: number,
}


const a : A = { str: '' }; // Works

const b : B  = { str: '' }; // Works

const c : C  = { str: '' }; // Works

const d : A = b; // Error

const e : A = c; // Error

@AugustinLF

What you're looking for here are exact objects

Your explanation makes totally sense to me, and therefore, it sounds logical that exact objects would be the safest thing to do here. However, for some reasons, I couldn't get it to work through the following example:

'use strict';

// @flow


/*::
type User = {
  name: string,
  age?: number,
  city?: string
};
*/

function greetUser(user/*: User */) {
  let greeting = `Hi ${user.name}!`;
  if (user.age) {
    greeting += ` You are ${user.age} years old.`;
  }
  if (user.city) {
    greeting += ` You live in ${user.city} city.`;
  }
  return greeting;
}


const user /*: {| name: string |} */ = { name: 'my name' };
greetUser(user);

Here, Flow complains about:

禄 flow
Error: index.js:27
 27: greetUser(user);
     ^^^^^^^^^^^^^^^ function call
 14: function greetUser(user/*: User */) {
                                ^^^^ property `age`. Property not found in
 27: greetUser(user);
               ^^^^ object type

Error: index.js:27
 27: greetUser(user);
     ^^^^^^^^^^^^^^^ function call
 14: function greetUser(user/*: User */) {
                                ^^^^ property `city`. Property not found in
 27: greetUser(user);
               ^^^^ object type


Found 2 errors

However, changing

const user /*: {| name: string |} */ = { name: 'my name' };
greetUser(user);

to

const user /*: { name: string } */ = { name: 'my name' };
greetUser({ name: user.name });

seems to please Flow.

Isn't the exact object annotation supposed to make the two above equivalent?

Hmmm, no, the exact object annotation makes it sure that there are no other properties, and in our initial case, that you won't find the same property with a different type (which is the reason why there was this subtype problem).

In your last comment, the {| name: string |} means that there can't be any age or city property, so {| name: string |} and { name: string, age?: number} are not compatible.

I'm not totally sure of what you want to do, in your last example, removing the typings of the const user works.

In your last comment, the {| name: string |} means that there can't be any age or city property, so {| name: string |} and { name: string, age?: number} are not compatible.

Could you explain what makes them incompatible? The former should be a subtype of the latter shouldn't it? If that's not the case, then why does greetUser({ name: 'a string' }) work? { name: 'a string' } could be typed as {| name: string |} right?

I'm not totally sure of what you want to do, in your last example, removing the typings of the const user works.

Yep, I wouldn't add a type touser in this case, I'm just making an example reproducing what I believe is happening in a more complex situation.
For example, one issue I'm currently facing is:

  • I have an imported function fn(args: A)
  • understand the type A as { p1?: string, p2?: string, p3?: string, p4?: string }.
  • in my code, I have to type the output of a library that doesn't have any type annotation
  • that library returns an object b of type B defined as { p1: string, p2: string }
  • when calling fn(b), Flow complains about the non-existence of p3 and p4 in b.
  • changing the type B to {| p1: string, p2: string |} doesn't change anything
  • however, calling fn() as fn({ p1: b.p1, p2: b.p2 }) makes Flow is happy.

I thought the example I originally wrote would make things a little easier to understand. Sorry if it was misleading.

Am I missing and/or misunderstanding something?

I got to play a little more with this and noticed two things. Let me use my original example and slightly modify it:

'use strict';

// @flow


/*::
type ShortUser = {
  name: string,
};
type User = {
  name: string,
  age?: number,
  city?: string
};
*/

function greetUser(user/*: User */) {
  let greeting = `Hi ${user.name}!`;
  if (user.age) {
    greeting += ` You are ${user.age} years old.`;
  }
  if (user.city) {
    greeting += ` You live in ${user.city} city.`;
  }
  return greeting;
}


const user /*: * */ = { name: 'my name' };
greetUser(user);

// Error on the line below. The type of `age` is incorrect
const wrongSubUser/*: User */ = { name: 'John', age: true };

// No problem with the 2 lines below
const subUser/*: User */ = { name: 'John' };
const subUserAlias/*: ShortUser */ = subUser;

// This works
greetUser(subUser);

// This generates an error: the properties `age` and `city` are said to be missing
greetUser(subUserAlias);

The first thing I noticed is that, by using the existential type * for user, Flow types it same as User. It seems that it uses the type of the first argument of greetUser().

The second thing I noticed is that despite Flow seems to consider ShortUser as a subtype of User, it wouldn't allow calls to greetUser() with an argument of type ShortUser.

Could it be that Flow is ok with subtype relations but doesn't do this resolution on function calls and require types to be strictly equals?

The second thing I noticed is that despite Flow seems to consider ShortUser as a subtype of User, it wouldn't allow calls to greetUser() with an argument of type ShortUser.

It's the other way around, ShortUser is a supertype of User

@vkurchatkin

It's the other way around, ShortUser is a supertype of User

You're totally right! I realized afterwards that something went wrong while I was copy-pasting back and forth between my editor and here. My apologizes.
Below is the (simplified) code I originally intended to post:

'use strict';

// @flow


/*::
type ShortUser = {|
  name: string,
|};
type User = {
  name: string,
  age?: number,
  city?: string
};
*/


function greetUser(user/*: User */) {
  let greeting = `Hi ${user.name}!`;
  if (user.age) {
    greeting += ` You are ${user.age} years old.`;
  }
  if (user.city) {
    greeting += ` You live in ${user.city} city.`;
  }
  return greeting;
}


const user = { name: 'user' };
greetUser(user);

const shortUser/*: ShortUser */ = { name: 'short user' };
greetUser(shortUser);

First of all, just to make sure I'm not missing anything:

  • ShortUser is limited to objects with one single key, being name and its value being a string
  • User is any object containing:

    • a name key whose value is a string

    • possibly an age key whose value would be a number

    • possibly a city key whose value would be a string

    • any other key whose value could be anything

ShortUser's set of possible values being a subset of User's possible values, ShortUser is a subtype of User here right?

If the above is correct, then why doesn't Flow let me call greetUser() with shortUser ?

Sorry if I'm missing something obvious.

It is possible to come to wrong conclusions If you think about subtypes just as subsets of possible values. You also have to consider a set of possible operations. For example, if operation O is allowed on type A and not allowed on type B , then B can not be a subtype of A.

For example, type A = { foo: string, bar?: string } allows deleting property bar, where type B = { foo: string } does not. Hence B is not a subtype of A. In this case it's not even a subset of values, because A contains only those values B that either don't have property bar or it is a string.

Right, thanks for the explanations! :)

Was this page helpful?
0 / 5 - 0 ratings