Flow: Flow does not infer map values types correctly

Created on 20 Oct 2018  Â·  5Comments  Â·  Source: facebook/flow

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 ?

All 5 comments

Flow is not able to infer that A is string | number in 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 } and
  • U = { +[string]: string | number }.
  • V = { [string]: string | number }.

If you have a value x: T, here are the things that you can do:

  • get x.a, which must be a string;
  • get x.b, which must be a number;
  • set x.a, which may be any string;
  • set x.b, which may be any number.

If you have a value x: U, here are the things that you can do:

  • get an arbitrary property, which must be a string or a number.

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:

  • get an arbitrary property, which must be a string or a number;
  • set an arbitrary property, which may be any string or number.

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 pick does 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 A to 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.

Was this page helpful?
0 / 5 - 0 ratings