Flow: Exact object spread too lax

Created on 7 Jun 2018  路  14Comments  路  Source: facebook/flow

Copying from @valscion in https://github.com/facebook/flow/issues/2405#issuecomment-379178749

// @flow
type A = {| a: string |};
type B = { b: string };
type C = {| c: string |};

declare var a: A;
declare var b: B;
declare var c: C;

var b: B = { b: 'foo', j: 'I am an extra property!' };

const d = { ...a };
const e = { ...b };
const f = { ...a, ...b };
const g: { a: string, b: string } = { ...a, ...b };
// This case SHOULD trigger an error, but it does not!!
const h: {| a: string, b: string |} = { ...a, ...b }; 
// This case should not trigger an error
const i: {| a: string, c: string |} = { ...a, ...c };

Try flow

cc @mrkev

spread bug

Most helpful comment

Ah, good ol' spread.

Objects are getting a bit of an overhaul that will manifest in a number of spread, class, etc. related issues being fixed. It's slowly coming about, but yeah, people are aware spreads are a little broken right now (especially with exacts).

All 14 comments

Sweet, thanks for opening this issue. For reference, here is a minimal example of the error: Try flow.

First bad commit is 3ff1fba844b6df378f4bab27caa7dd7023ff48d1.

Another manifestation of what I believe is the same problem. Problem appears between v0.69.0 and v0.70.0.

Hi @rattrayalex-stripe !!

We are running into this issue and are wondering if there is any progress to get a fix for this merged in.

Hope all is well,
Alex

Ah, good ol' spread.

Objects are getting a bit of an overhaul that will manifest in a number of spread, class, etc. related issues being fixed. It's slowly coming about, but yeah, people are aware spreads are a little broken right now (especially with exacts).

Sounds good @mrkev - is there an umbrella issue or something for this where we can follow progress and what is being tinkered with?

Unfortunately not that I know of /:

@valscion @rattrayalex-stripe
I have two comments:
1) Why does this case should have an error?

type A = {| a: string |};
type B = { b: string };
declare var a: A;
declare var b: B;
// This case SHOULD trigger an error, but it does not!!
const h: {| a: string, b: string |} = { ...a, ...b };

{| a: string, b: string |} is a supertype of { a: string, b: string }, so using h variable will be safe.

This case can be compared with next:

const h: {} = { a: '', b: '' };

You're expanding { a: string, b: string } type to {}.
The converse is wrong: we aren't allowed to narrow the type:

const h: { a: string, b: string, c: string } = { a: '', b: '' }; // has error

because { a: string, b: string, c: string } is a subtype of { a: string, b: string }.

Another example:

const a: number = 1
const h: string | number = a;

string | number is a supertype of number. It is safe because below in your code you can refine type:

if (typeof h === 'string') { /* to do something with string */ }

2) Also this case:

// This case should not trigger an error
const i: {| a: string, c: string |} = { ...a, ...c };

doesn't have errors now (check here, please).

@oonsamyi,

  1. Why does this case should have an error?
type A = {| a: string |};
type B = { b: string };
declare var a: A;
declare var b: B;
// This case SHOULD trigger an error, but it does not!!
const h: {| a: string, b: string |} = { ...a, ...b };

Beceause b may be { b: "hi", c: "uh oh" }, in which case h will
have an attribute c that is not listed in its exact object type. This
is unsound.

{| a: string, b: string |} is a supertype of { a: string, b: string }, so using h variable will be safe.

No: the opposite is true. It is a _subtype_. Observe:

// @flow
type A = {| a: string |};
type B = { a: string };
declare var a: A;
declare var b: B;
(a: B);  // OK (good)
(b: A);  // error (good)
  1. Also this case:
// This case should not trigger an error
const i: {| a: string, c: string |} = { ...a, ...c };

doesn't have errors now

It never did; this was an example of Flow behaving correctly.

This is unsound.

And, to be more explicit, this yields coerce:

// @flow
function coerce<T, U>(t: T): U {
  type Box = {| +isA: true, +t: T |} | {| +isB: true, +b: string |};
  const box: Box = {
    isA: true,
    t: t,
    ...({ isB: true }: {})
  };
  if (box.isB && box.b === undefined) {
    return (box: empty).t;
  }
  throw new Error("Unreachable.");
}
const twelve: number = coerce("twelve"); // no type error!
twelve.toFixed(); // runtime error!

(Typechecks, but fails at runtime, on Flow v0.85.0 and master.
Introduced in v0.70.0.)

@wchargin
Yes, I understood the error case.
I'm confused.

No: the opposite is true. It is a subtype.

Can you see this example: https://flow.org/en/docs/lang/subtypes/#toc-subtypes-of-objects
ObjectB is a subtype of ObjectA => ObjectA is a supertype of ObjectB:

type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;

Can I said same about exact type?

type ObjectA = {| foo: string |};
type ObjectB = { foo: string, bar: number };

that ObjectA is a supertype of ObjectB.
Or is it wrong?

ObjectB is a subtype of ObjectA => ObjectA is a supertype of ObjectB:

type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;

Yes: in that example, ObjectB is indeed a subtype of ObjectA.

Can I said same about exact type?

type ObjectA = {| foo: string |};
type ObjectB = { foo: string, bar: number };

No: in that example, ObjectB is _not_ a subtype of ObjectA.

Please recall what it _means_ for U to be a subtype of T: it means
that every value of type U is also a value of type T. There are
values of type ObjectB that are not of type ObjectA, and in fact
there are values of type ObjectA that are not of type ObjectB, so
neither is a subtype (or supertype) of the other.

Thank you!
Yes, I agree:

There are values of type ObjectB that are not of type ObjectA

But I don't understand this assertion:

there are values of type ObjectA that are not of type ObjectB

ObjectA has only prop foo, that is string, but ObjectB has it too.
In accordance with

U to be a subtype of T: it means that every value of type U is also a value of type T

ObjectA is a subtype of ObjectB.

But I don't understand this assertion:

there are values of type ObjectA that are not of type ObjectB

The value { foo: "hello" } is of type ObjectA but not ObjectB, as
you can see in Flow Try
.

Therefore, ObjectA cannot be a subtype of ObjectB.

Was this page helpful?
0 / 5 - 0 ratings