I'm having a difficulty with a flow-typed libdef, but I'm wondering if it's not a Flow bug:
This is the flow-typed issue : https://github.com/flow-typed/flow-typed/issues/2846
The code I'm trying to make work is this:
/* @flow */
declare function pick<A>(keys: Array<string>, val: { [key: string]: A }): { [key: string]: A };
// This does not work
const foo: {a: string, b: number} = { a: 'foo', b: 42 }
pick(['a', 'b'], foo)
// But this works
const foo2: {a: string, b: string} = { a: 'foo', b: '42' }
pick(['a', 'b'], foo2)
// And this works too
const foo3: {a: string, b: any} = { a: 'foo', b: 42 }
pick(['a', 'b'], foo3)
Try link: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVATApgYxgQwCcswoBXAOxwBcBLOCsAB1pwGsAeAQQD4AKNlgCeAZwBcYLoUL4hHEdUK0KAcx4AaMADd8MCQG8wAbUFCJCpaoC6ErmAC+ASgPHT5xcpU3JDgNyowMFRgYDAAFQALWhEwDDgsGIo4ajAEOEI2VBwGBVI4OAN8d0sVTQAjCQoyAFsyrEJ7MABeMEMisAByKHyO8okAFgAmB1QWdj4jDvxezrKOq01uuEd0ELAAITIU6iiYtIyRLJyUpcHC4s8+sAtPRpa2iS6eq46hjpGxtgmpmY65hbycEGK2CoS4FAwYB20VS6TYMWo+SOFFySwAzOdrh5VFd8BQhHdWmB2k84DMKmAhh9WF9JtNNH95ot8mjHEA
I don't understand why Flow is not able to infer that A is string | number in the first case.
It seems that when Flow encounters a { [key: K]: V } it expect that all values of the map have the same type ... And even, that doesn't explain why it works with any ...
Can someone explain this behavior ?
Flow is not able to infer that
Aisstring | numberin the first
case.
Flow is correct here.
You’ve declared that foo has type {a: string, b: number}. This is
_not_ a subtype of { [key: string]: string | number }. What if pick
were implemented like this?
function pick<A>(keys: Array<string>, val: { [string]: A }): { [string]: A } {
val[keys[0]] = val[keys[1]]; // surely valid: both of type `A`
return val;
}
Then, after the call to pick, foo would be { a: 42, b: 42 }, even
though its type is still { a: string, b: number }. This is unsound.
Note that this problem only arises if pick can mutate the elements in
val. If you declare that pick does not do this, then your code
typechecks:
// @flow
declare function pick<A>(
keys: Array<string>,
val: { +[key: string]: A }
): { +[key: string]: A };
const foo: { a: string, b: number } = { a: "foo", b: 42 };
pick(["a", "b"], foo);
const foo2: { a: string, b: string } = { a: "foo", b: "42" };
pick(["a", "b"], foo2);
const foo3: { a: string, b: any } = { a: "foo", b: 42 };
pick(["a", "b"], foo3);
As for why it works with any… well, when you use any, you lose type
safety. Flow is happy to convert your type { a: string, b: any } to
type { a: string, b: string } implicitly, even though this is unsound.
That’s just how any works, and why it should be avoided.
You’ve declared that foo has type {a: string, b: number}. This is
not a subtype of { [key: string]: string | number }. What if pick
were implemented like this?
Of course ! Thank you for highlighting this ! It's because with { [key: string]: string | number } we're saying that every value of map can be either string or map. So Foo would have to be { a: string | number, b: string | number }
But then, I don't get why your example works ... Is { a: string, b: number } a subtype of { +[key: string]: string | number } ? I guess not.
I don't understand it works without mutation (and actually pick does not not mutate, so I'm very interested in understanding this.
What does Flow infer A to in :
// @flow
declare function pick<A>(
keys: Array<string>,
val: { +[key: string]: A }
): { +[key: string]: A };
const foo: { a: string, b: number } = { a: "foo", b: 42 };
pick(["a", "b"], foo);
?
I don't get why your example works ... Is
{ a: string, b: number }a
subtype of{ +[key: string]: string | number }? I guess not.
It is, and this is why the example works.
You can ask Flow to verify this:
// @flow
function check(x: { a: string, b: number }): { +[string]: string | number } {
return x; // typechecks
}
For brevity, let’s define
T = { a: string, b: number } andU = { +[string]: string | number }.V = { [string]: string | number }.If you have a value x: T, here are the things that you can do:
x.a, which must be a string;x.b, which must be a number;x.a, which may be any string;x.b, which may be any number.If you have a value x: U, here are the things that you can do:
Thus, anything that we can do to a U we can also do to a T, so it is
safe for T to be a subtype of U.
If you have a value x: V, here are the things that you can do:
Thus, if you have a V, you can do something to it that you _cannot_ do
to a T: you can set a to a number. So T cannot safely be a subtype
of U.
This is the Liskov substitution principle.
and actually
pickdoes not not mutate, so I'm very interested in
understanding this.
If the type is declared as accepting a mutable argument, then it doesn’t
matter whether you actually mutate it or not. (For instance, maybe you
want to change the function in the future so that it _does_ mutate the
argument—changing its implementation without changing its type must not
cause a type error in client code.)
This is similar to the fact that in the following function
function print(x: string) {
console.log(x);
}
it is not valid to invoke print(3), even though this would be
completely safe _with the current implementation_.
What does Flow infer
Ato in : [snip]
It infers string | number. You can verify that this suffices by
specifying the type parameter at the call site:
pick<string | number>(["a", "b"], foo); // typechecks
Does this answer your question, or is something still unclear?
Thank you for the time to put at making this so clear !
And sorry for the mistaken issue !
I can go back writing better typings ;-)
No problem, and you’re quite welcome. Happy to help.