Flow: Intersection doesn't work for exact object types

Created on 13 Oct 2016  Â·  16Comments  Â·  Source: facebook/flow

Given the following example:

declare type Foo = {| foo: string |} & {| bar: string |}
const example: Foo = {foo: 'foo', bar: 'bar'}

I expect this to work since the intersection type should be allowed to further extend the exact object. But in the latest version, it errors.

Is there an easy way to make this work or is this intentional?

Most helpful comment

@samwgoldman is working object type spread, I guess it should work with exact type as well:

type Foo = {| foo: string |};
type Bar = {| bar: string |};
type FooBar = { ...Foo, ...Bar };

All 16 comments

If value v has type A & B that means that it has type A and has type B at the same type. If A is {| foo: string |} it means that v has exactly one property foo and if B is {| bar: string |} it means that v has exactly one property bar. Obviously, you can't satisfy these two constraints at the same time, so A & B is an empty type.

Thank you for the answer. Your explanation totally makes sense but I was hoping to build an exact object type using intersection somehow. The above example obviously works with a regular object type, so I expect the exact object to behave in a similar matter (so that the outcome equals declare type Foo = {| foo: string, bar: string |}.

Do you know of another why I could model this behaviour? I have two types, A and B, who define keys and values and I want to build an intersection A & B as an exact object, so that _only_ the keys defined in either A or B are valid.

I'm pretty sure there is no way do it at the moment.

Chiming in -- just ran into this myself.

Terminology aside, do you think "combining exact types" is a possibility at some point?

Going back to the explanation above, let's say I have an exact object type A and I want to create a new (preferably strict) type C that's a superset of A (we'll call the additional fields object type B).

Let's also say that A is used elsewhere. If I want to create this new type without duplicating all of the same fields included as part of the exact object type A, I'm forced to relax the strict/exact check.

To provide a more concrete example, I'm attempting to do the following:

// Used elsewhere as a standalone type.
export type MyStrictType = {|
  uri: string,
  name: string,
  type: string
|};

export type Action =
  // ... others
  | ({ type: 'SOME_CONST', anotherAttr: string } & MyStrictType)
;

Admittedly, in addition to relaxing the strict/exact object requirement, one way I could fix the above is by instead introducing a nested attribute of { type: 'SOME_CONST', anotherAttr: string } (which I've referred to above as object type B) rather than trying to combine the two.

That's how I've fixed "the problem" for now, because that's better than the alternative of relaxing the exact object type for MyStrictType (which allows for much better "save me from myself" Flow checking where it's used elsewhere).

I just wanted to provide a more concrete example of why it'd be desirable so that it's a little less contrived/abstract for the sake of a discussion. Mathematically, I'm not sure what the term would be for this relationship, but it'd be a welcomed addition! 😄

@samwgoldman is working object type spread, I guess it should work with exact type as well:

type Foo = {| foo: string |};
type Bar = {| bar: string |};
type FooBar = { ...Foo, ...Bar };

@vkurchatkin That seems like the simplest way to express this without loosing the meaning of $Exact as something that doesn't like to be extended.

IMHO that'd be a much welcomed addition!

It doesn't get much simpler than that (from a user's perspective anyway)! 🎉

Yeah, @vkurchatkin is exactly right on all counts. It's not possible to satisfy two different exact object types simultaneously. The root of the issue is that intersections of objects isn't a proper "merge" operation — it just behaves like that in many cases, and has become idiomatic. Proper object type spread, which I am working on, is a better solution.

Seems related to https://github.com/facebook/flow/issues/1326

I'm very interested in this topic resolution!

@vkurchatkin, the object type spread feature is a great step forward, but it doesn't handle the perhaps more common scenario where you want to throw an error if the types aren't compatible with each other. For example, in a TypeScript interface:

interface Foo {
    baz: string;
}

interface Bar {
    baz: number;
}

interface Qux extends Foo, Bar {}

:red_circle: Interface 'Qux' cannot simultaneously extend types 'Foo' and 'Bar'. Named property 'baz' of types 'Foo' and 'Bar' are not identical. – interface Qux.

This is ideal, because it prevents unintended side effects, which is kind of the whole point of a type system.

Personally, I would prefer that strict object types are the default Flow behavior #3214 and Excess Property Checks are used to achieve Flow's current default behavior.

@jedmao's concern is definitely relevant to my plan for using this feature (if object spread for types were the way to solve it).

Closing. Now you can do this:

declare type Foo = {| ...{| foo: string |}, ...{| bar: string |} |}
const example: Foo = {foo: 'foo', bar: 'bar'}

However, this still doesn't:

type One = {| n: number |}
type Two = {| bar: string |}
type Foo = {| ...One, ...Two |}

const one: One = {n: 1}
const two: Two = {bar: 'bar'}
const example_OK: Foo = {n: 1, bar: 'bar'}
const example_NOT_OK: Foo = {...one, ...two}

Hi,
I did not find this awesome feature in the doc ? Did i miss it ?

@AlexandreBossard no you did not miss it. It is simply undocumented.

I think I am hitting the same issue, @matthewjohnston4.

https://github.com/facebook/flow/issues/6526

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mjj2000 picture mjj2000  Â·  3Comments

tp picture tp  Â·  3Comments

bennoleslie picture bennoleslie  Â·  3Comments

philikon picture philikon  Â·  3Comments

jamiebuilds picture jamiebuilds  Â·  3Comments