/* @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 ?
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 anyageorcityproperty, 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
constuser 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:
fn(args: A)A as { p1?: string, p2?: string, p3?: string, p4?: string }.b of type B defined as { p1: string, p2: string }fn(b), Flow complains about the non-existence of p3 and p4 in b.B to {| p1: string, p2: string |} doesn't change anythingfn() 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,
ShortUseris a supertype ofUser
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 stringUser is any object containing:name key whose value is a stringage key whose value would be a numbercity key whose value would be a stringShortUser'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! :)