Flow: Document object type spread

Created on 17 Mar 2017  Â·  13Comments  Â·  Source: facebook/flow

Given:

type A = {
  a: number;
}
type B = {
  b: number;
  ...A;
}

I'd expect B to be equivalent to:

type B = {
  b: number;
  a: number;
}

But instead it's:

type B = {
  b: ?number;
  a: ?number;
}

Why? Is that the intended behavior?

Tested in v0.42.0 and master: https://flowtype.org/try/#0C4TwDgpgBAglC8UDeAoKUCGAuKA7ArgLYBGEATgNwoC+VokUAQgsmlMTgSeVegHQCYVamxQBjAPa4AzsCgAPHHERJMOAIxRa4qbKggczFWqj5cAEwgAzAJa4I5gDTscZy7fvmtVIA

Needs docs

Most helpful comment

This seems to be the intended behaviour indeed. Basically, spread type is a type of result of object spread operation. Object spread only copies own properties, but object properties can also be inherited. Another thing to remember is that non-exact objects can have any properties other than defined.

So here are the rules:

type A = {
  a: number
}

type B = {
  b: number,
  ...A
}

// The same as

type B = {
  a?: number, // every property from spread becomes optional
  b: mixed // every property before non-exact spread becomes mixed
};
type A = {
  a: number
}
type B = {
  ...A,
  b: number
}

// The same as

type B = {
  a?: number,
  b:  number // properties after spread stay the same
};
type A = {
  a: number
}

type B = {
  b: number,
  ...$Exact<A>
}

// The same as

type B = {
  a: number,
  b:  number
};

$Exact fixes both problems, because exact objects are not allowed to have additional properties and implies that all properties are own.

All 13 comments

It seems to be more like { b: mixed } in lower bound position:

({b: 0 }: B); // works
({b: '' }: B); // also works

This seems to be the intended behaviour indeed. Basically, spread type is a type of result of object spread operation. Object spread only copies own properties, but object properties can also be inherited. Another thing to remember is that non-exact objects can have any properties other than defined.

So here are the rules:

type A = {
  a: number
}

type B = {
  b: number,
  ...A
}

// The same as

type B = {
  a?: number, // every property from spread becomes optional
  b: mixed // every property before non-exact spread becomes mixed
};
type A = {
  a: number
}
type B = {
  ...A,
  b: number
}

// The same as

type B = {
  a?: number,
  b:  number // properties after spread stay the same
};
type A = {
  a: number
}

type B = {
  b: number,
  ...$Exact<A>
}

// The same as

type B = {
  a: number,
  b:  number
};

$Exact fixes both problems, because exact objects are not allowed to have additional properties and implies that all properties are own.

That's brilliant, thanks for this super useful addition to the language. I've been longing for this for quite some time. Shouldn't you write a blog post or something? This is really a killer feature.

Sorry, last week got away from me and I didn't have docs prepared for the release of 0.42. Working on it, though.

@vkurchatkin Let me see if I understood correctly. In your example…

type A = {
  a: number
}
type B = {
  ...A,
  b: number
}

// The same as

type B = {
  a?: number,
  b:  number // properties after spread stay the same
};

…an object called foo may match A with an inherited property a. However, when that same object is spread (e.g. { ...foo, b: 3 }) that inherited property would not be copied over. Flow tries to mimic that behaviour by making a optional, is that right?

@guigrpa yes, sounds right

I have a follow up question that I think is related. I'm trying to use object type spread to increase the strictness of an object, but there's behavior that I don't understand.

Here are my types:

type Action = {
  type: string,
  payload?: any
}

type MyAction = {
  payload: any,
  ...$Exact<Action>
}

I'm hoping that by doing this, MyAction no longer has an optional payload property.

However, when I do this:

const y: Action = {
  type: 'ACTION'
};

(y: MyAction);

Why doesn't flow throw an error on y: MyAction.

Here's the flow.org/try link: https://flow.org/try/#0C4TwDgpgBAggxsAlgewHZQLxQN4CgpSiQBcUAzsAE6KoDmANPlGAIYgA2yLAJgPyktUIXAF9cuItACyIeEjSYcTVhy7cBQxgQB0ugCQBRAB4sEAHjkpUAPlFNcAegdQA8gGs2hABaIyUAG4s7IjcLMAQfix+AJJQEEaQCLhwaBRQRqQylgpYeASSpADkMADCACrRLgByhVrMbJw8pNgijCIA3OJOsKjcUIjAUHBRwJExcQkQSQAUGbAIVgCUnV3OAEYAroOIAGZQsVQghMhQ3CfAXtCUEP4QlGQQyamDIKTZ6LlMBVDF5ZU1ohW3Vi8USgwuvmO3koyAA7lBBHFKDDKFA1lMWBsHlAjmcIqhCoMvCxbj8VI1uIUEb1+oM8WQCcBcNNXlAsgs0MsgA

@lewisf I agree, this looks like a bug

EDIT: I just noticed the post right above about strictness ;). Heres perhaps another example that ultimately tries to use the lenient type (e.g. relay mutation)

We start with lenient base types, then start restricting them in our components. I could go into more details, but it may be too much context, so I'll let the example speak for itself:

type Lenient = {
  a?: number,
  c: string
}
type Restrictive = {
  ...$Exact<Lenient>,
  a: number,
}

function foo(bar: Lenient): string {
  return 'baz'
}

const r: Restrictive = { a: 1, c: 'foo' };

// Cast from Restrictive to Lenient fails
const l = (r: Lenient)

// Restrictive is incompatible with Lenient
foo(r)
17: const l = (r: Lenient)
               ^ object type. This type is incompatible with
17: const l = (r: Lenient)
                  ^ object type
20: foo(r)
        ^ object type. This type is incompatible with the expected param type of
10: function foo(bar: Lenient): string {
                      ^ object type

I'm struggling to see why foo() won't accept the more lenient type, and why I cannot cast from Restrictive to Lenient.

This is a big problem for us, what part of object type spread am I missing?

Using $Supertype seems to workaround my issue...but I'd like someone to tell me for sure that it is doing what I expect. $Supertype is undocumented so I can't tell if this is as intended.

@rosskevin I'm not sure if this is the Correct answer, but it works if you make the "lenient" field covariant, eg +a?: number instead of a?: number.

Note that const l = (r: Lenient) no longer fails; I'm not sure whether that is desired or not.

It seems to work differently with exact types:

// @flow
type A = {|
  a: number,
  aa: string
|}

type B = {|
  b: boolean,
  bb: Array<string>
|}

type AB = {| ...A, ...B |}

const ok: AB = {a: 1, aa: 'aaa', b: false, bb: ['ya', 'yaa']} // works


// uncomment for errors

// const extraField: AB = {a: 1, aa: 'aaa', b: false, bb: ['ya', 'yaa'], badField: 321}

// const missingField: AB = {a: 1, b: false, bb: ['ya', 'yaa']}

Try Flow

@vkurchatkin I think your explanation would make sense if we were talking about JS objects. But in this case we are spreading _types_ right? As seen in the original post all the props are well defined so IMO it's clear that the spreading should work like explained in the original comment. There is no risk that there would leak some magical types that overrides some properties. The behaviour where something becomes optional or mixed ruins the idea of spreading and is very confusing. Am I missing something here?

Fixing by using exact does not sound like a solution to me. As far as I know, non-exact means that the _object_ can contain some other values that the developer is not interested in (e.g they just flows trough the system without dev needing to know about them). But the _types_ still can't contain anything else than what's defined so it should be very clear what has been spread and what has not been spread.

Was this page helpful?
0 / 5 - 0 ratings