Flow: { prop: <string literal> } should satisfy { prop: string }

Created on 21 Jul 2015  路  4Comments  路  Source: facebook/flow

I may have misunderstood how disjointed unions should work with bounded polymorphism but consider this:

/* @flow */

type Action = { type: string };
type MyAction = { type: 'LOL' } | { type: 'OMG' }; // A subset of Action, isn't it?

function dispatch(action: Action) {
  console.log(action);
}

var action: MyAction = { type: 'LOL' }; // works if I change to : Action
dispatch(action);

Actual output:

index.js:11:1,16: function call
Error:
index.js:3:23,28: string
Expected string literal LOL
index.js:4:25,29: string literal LOL

index.js:11:1,16: function call
Error:
index.js:3:23,28: string
Expected string literal OMG
index.js:4:43,47: string literal OMG

Found 2 errors

I'd expect it to pass type check because MyAction is a subset of Action (every possible form of MyAction satisfies Action).

Most helpful comment

Closing, this can be fixed using covariant property:

type Action = { +type: string }; // <-- here it is
type MyAction = { type: 'LOL' } | { type: 'OMG' }; // A subset of Action, isn't it?

function dispatch(action: Action) {
  console.log(action);
}

var action: MyAction = { type: 'LOL' };
dispatch(action);

All 4 comments

I'm actually wrong. It doesn't have anything to do with either disjoint unions, or bounded polymorphism :-P

This is enough to reproduce the issue:

/* @flow */

type Action = { type: string };
type MyAction = { type: 'LOL' };

function dispatch(action: Action) {
  console.log(action);
}

var action: MyAction = { type: 'LOL' };
dispatch(action);

I get:

index.js:11:1,16: function call
Error:
index.js:3:23,28: string
Expected string literal LOL
index.js:4:25,29: string literal LOL

Found 1 error

I'd expect it to pass the type check because clearly a string literal is also a string.

Just from the error message, it looks to me like flow is unifying the type Action and MyAction in the function call instead of using subtyping.

And, actually, I think that we need unification here, because of mutability. Consider the following example:

type Action = { type: string };
type MyAction = { type: 'LOL' };

function dispatch(action: Action) {
  // action.foo is just string, so assignment is valid locally
  // however, the passed-in value is now mistyped
  action.foo = "bar";
}

var action: MyAction = { type: 'LOL' };
dispatch(action);

I would really like to see flow accept the kind of code you've written (while rejecting the code in my example), and I can see a couple ways to get there:

  1. Some kind of "const" annotation, that prevents objects from being modified. Note that this is different from const bindings as in let/const.
  2. Instead of annotating the const, flow could infer whether a function mutates its argument, and if no mutations are made, then subtyping rules are safe (and permit more valid programs!)

However, neither of these is simple to implement...

I'll also note that the issue title is a bit misleading. { prop: "literal" } is a subtype of { prop: string }, as my example below shows. The real issue here is that flow is pessimistic about when this subtyping can apply.

/* @flow */

declare function test(foo: { bar: string }): void;

// Object literals are safe to pass in
test({ bar: "baz" }); // OK

// Even if there is an alias, because the type is unconstrained
var x = { bar: "baz" };
test(x); // OK

// But if we specify a more specific type, we need to ensure consistency
var y: { bar: "baz" } = { bar: "baz" };
test(y); // Error

Closing, this can be fixed using covariant property:

type Action = { +type: string }; // <-- here it is
type MyAction = { type: 'LOL' } | { type: 'OMG' }; // A subset of Action, isn't it?

function dispatch(action: Action) {
  console.log(action);
}

var action: MyAction = { type: 'LOL' };
dispatch(action);
Was this page helpful?
0 / 5 - 0 ratings