Flow: Unexpected behavior of union types

Created on 23 May 2018  ·  11Comments  ·  Source: facebook/flow

The following types work as expected:

type Foo = number | string;
type Bar = Foo & number;

const bar: Bar = 123; // No error here

Try

And I expected the same behavior with an object structure but it causes an error:

type Foo = {
  foo: number | string,
};

type Bar = Foo & {
  foo: number,
};

// $ExpectError Woops! string is incompatible with number. Why you no work?!
const bar: Bar = {
  foo: 123,
};

Try

What am I doing wrong?

unionintersections

Most helpful comment

@apsavin (string | number) & number is equivalent to (string & number) | (number & number). This expressions means that value should fit either string & number _or_ number & number.

Of course string & number is an impossible type while number & number (which is actully equivalent to number) can be satisfied with any numeric value.

So if you assign a number to a variable of type (string & number) | (number & number) the left branch of | is ignored while the right branch is satisfied. So there's certainly no error here 🤓

But the original question is still unanswered: why this logic is not applied to values nested in objects?

All 11 comments

Just found out that changing the order of & matters and error disappears:

type Foo = {
  foo: number | string,
};

type Bar ={
  foo: number,
} & Foo; // Order is changed!

// Wow! No error here! But why?
const bar: Bar = {
  foo: 123,
};

Try

Can please someone explain this?

Well, as far as I understand, all the strange things could be explained with one simple statement:

Type (string | number) & number has no sense. It's impossible to get a variable of type string & number in JavaScript.

Of course, I think, flow should handle this error better.

@apsavin (string | number) & number is not equivalent to string & number. This is perfectly fine with Flow:

let a: (string | number) & number = 123;

Try

Well, _this_ looks like an error for me.

@apsavin (string | number) & number is equivalent to (string & number) | (number & number). This expressions means that value should fit either string & number _or_ number & number.

Of course string & number is an impossible type while number & number (which is actully equivalent to number) can be satisfied with any numeric value.

So if you assign a number to a variable of type (string & number) | (number & number) the left branch of | is ignored while the right branch is satisfied. So there's certainly no error here 🤓

But the original question is still unanswered: why this logic is not applied to values nested in objects?

Consider this. Let's say this assignment is allowed:

type Foo = {
  foo: number | string,
};

type Bar = {
  foo: number,
};

const v: Foo & Bar = {
  foo: 123,
};

Since v is of type Foo & Bar, we can cast it to both Foo and Bar:

const bar: Bar = v;
const foo: Foo = v;

Foo would allow a number to be written to foo property, while Bar expects only numbers there.

Hence, this assignment is unsound.

@vkurchatkin I see, this makes sense. But why there's no error when Bar and Foo are transposed?

type Foo = {
  foo: number | string,
};

type Bar = {
  foo: number,
};

const v: Bar & Foo = { // No error here 
  foo: 123,
};

Try

@vkurchatkin I also do not understand why the idea you described is not applicable to this case:

// @flow
type Foo = number | string;

type Bar = number;

let v: Foo & Bar = 123;  // No error

let bar: Bar = v;  // No error
let foo: Foo = v;  // No error

Try

But why there's no error when Bar and Foo are transposed?

Well, that looks like a bug.

I also do not understand why the idea you described is not applicable to this case

It's not applicable, because number is a subtype of string | number. This makes the intersection type non-empty

Foo would allow a number to be written to foo property, while
Bar expects only numbers there.

This completely makes sense, though it is at odds with the docs for
“Intersection of object types”
, which say

But when you have properties that overlap by having the same name, it creates an intersection of the property type as well.

and should probably be updated.

But shouldn’t the assignment be legal when the properties are marked
covariant?

type Foo = {
  +foo: number | string,
};

type Bar = {
  +foo: number,
};

// Still an error, even though properties are read-only.
const v: Foo & Bar = {
  foo: 123,
};

(Some similar discussion took place around #5895 including the
observation that the intersection bifunctor is not extensionally
symmetric.)

Well, that looks like a bug.

@vkurchatkin How can I mark this issue as a bug? Or should I create a new issue?

@smikhalevski yep, create a new issue and /cc me

Was this page helpful?
0 / 5 - 0 ratings