Flow: Exact type doesn't work with possible empty object

Created on 8 Dec 2016  Â·  48Comments  Â·  Source: facebook/flow

Repro

type A = {| a?: string |};
const x: A = {} // error: object literal. Inexact type is incompatible with exact type

Flow version 0.36

bug unsealed objects

Most helpful comment

There is the simplest example:

({}:{||})

All 48 comments

There is the simplest example:

({}:{||})

Related #2386

Exact types are really a key feature in flow, but so many puzzling issues with it.

@mroch @samwgoldman Should we avoid using exact types? Or are there any workaround to this issue? Would love to help...

@skovhus try https://github.com/facebook/flow/issues/4582#issue-249772183 for a workaround.

@chrisblossom thanks... I might be missing something, but the example linked to simply gets rid of the exact type.

@skovhus here is an example: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgCiYAvGAN5iphgCGA-AFxgDOGATgJYB2A5gDTUqNYMDAB1OOwDWtdnACu3ACZgoUsHgAetAMYYw2fADoJU6SzqXt+fXmVDRYBAAs83MMry0YPXs84MFzA4ACMAKzx9SyDaA11aD1CCPABbHGxjIQBtRKwAXWY0jKwqAF90XThuNk1mYjJyMrAnbjhNdnl2Fkrq2qh60gowUMYARjBmpzxOqUtaay1bDHsgA

Thanks @chrisblossom!

I've encountered the same issue and solved with $Shape<>.

type E = $Shape<{ 
  a: string,
}>

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVAXAngBwKZgCiYAvGACQDKAFgIb4A8A3mKmGLQFxgDOGATgEsAdgHMANKgC+APnQBjOML5g83YmSZSwwYGGFxV-fnH48FSlVHWkwLAEacAjGG26jJsxx6qAHvnkMPAATIA

Is this correct use?

I don't think you can use $Shape for this.

@skovhus I don't understand what you're saying the problem with $Shape is.

@jedwards1211 I was wrong. $Shape seems to work. : )

Thanks @shinout! I like this solution better than the [any]: empty trick (https://github.com/facebook/flow/issues/4582#issue-249772183).

@skovhus okay cool, I was wondering if there were issues on past versions of flow or something.

Please also note https://github.com/facebook/flow/issues/5884 when using the $Shape<T> workaround.

There is no official solution for this yet, right?

Flow 0.70 has new warnings with spreading non exact object which makes this more important to fix i think

This works for most use-cases I have: https://stackoverflow.com/a/44444793/3098651

(Object.freeze({}): {||})

This works:

type A = {| a?: string |};
const x: A = (Object.freeze({}))

I don't think this is a bug; it sounds like expected behaviour because {} is by default unsealed. Object.freeze will seal it.

https://flow.org/en/docs/types/objects/#toc-unsealed-objects

Has this been changed with Flow 0.72?

@mrkev I do not think the bug label should have been removed. You should not have to Object.freeze an object that has all optional keys but is set as exact.

Honestly this issue has been a major issue for me, and probably others. The workarounds all have caveats and do not always work as expected.

@chrisblossom I think the semantics are a little confusing to catch at first, but it's due to to sealing. I do recommend you give the docs on the matter another skim (:

The issue is if an object is unsealed, it can't be exact because you can add any key/value to it. This allows you to use objects as maps in a very javascript-y way.

What this issue essentially reduces to is that there's no good way of creating a sealed empty object, other than initializing it with some property and then deleting the property, or freezing it. I didn't close the issue because this can be considered a feature request, but I'd be hesitant to call it a bug because everything is working as expected.


EDIT: I do completely agree though, the whole notion of sealed vs unsealed objects and it being so transparent to the user is a little hard to navigate. Unsealed objects are necessary for compatibility though unintuitive IMO.

Also, for the sake of reference, this is what I mean by initializing with some property and deleting it:

type A = {| a?: string |};
const x: A = { a: "foo" } // sealed object
delete x.a; // x should now be empty

EDIT 2: I'll add the feature request flag

Thanks for your response!

There is a difference between inference (unsealed in this case) and specifying an exact object type.

These three object types are not equal, and two of the three are not unsealed:

const a = {}; // unsealed
const b: {||} = {}; // exact object type
const c: {} = {}; // sealed

This shows why it is a bug, and not a feature request.

https://flow.org/try/#0PTAEAEDMBsHsHcBQiSgHa1AUwE49jgM6gCGxWAHgA5YDGALlgCaK2xqH2gCuHWJ0ZqAC8oAN4BfANzJehfoKYA6SLEyiAjDLkLmSgEYkcI0PRzcs2vgL2GAXiYDk9ABY4sWRzJRhc+IqTk1HSMLGwcXJQkDABc4gA+8RImkt5RDCpqJlqI6fQGRiZmFjJ5BQ6izm4eXsiofgTEZNjBDMys7Jyg8jZMcZIp0sg9ipnqoDkjtoWixZaIU8r2Tq7unjJAA

Wait, sorry. I lost track of this issue and perhaps now I just don't have the context I used to before. How does your example demonstrate its a bug? Focusing on this:

const a = {}; // unsealed
const b: {||} = {}; // exact object type
const c: {} = {}; // sealed

{} is always inferred to be unsealed, which means it errors only on b.

Oh, I think I see what you mean;

const x: {a?: bool} = {} // no error
const y: {| a?: string |} = {} // error

But nonetheless

const x: {a?: bool} = {}
x.b = 4 // error

Hmmm that's a good question, and I'm not 100% sure if it's working as intended. Anyway, lets just call it a bug until proven otherwise ¯\_(ツ)_/¯

I'm not sure if x.b = 4 being an error above is the same issue; I'm tempted to believe it's another tangential one, but in any case, let's just keep both tags cause the ability to create an empty exact sealed object is an unavailable feature (that might fix a bug, esp considering it provides functionality that's important but unavailable atm).

const x: {a?: bool} = {}
x.b = 4 // error

Oh, that's weird :confused: because there is no error in this:

const x: {a?: bool} = { b: 4 };

It's not too weird.))

You can't add a property b to an object that have type {a?: bool} because it means that you want to treat this object as if it has some other type with property b.

But you can assign object { b: 4 } to a constant with type {a?: bool} because it's ok if you want to treat such object as {a?: bool}.

is there a complication for this to be fixed, or is there just no interest by the core team?
in the first case, what is it?
in the second case, then at least we know that its up to someone to contribute.

This issue is old and it is still real pain when typing React components as it prevents properties delecation with spread operator. What is the status? Any updates?

Another use case: I'm trying to store some results in a "map" like this:

opaque type Key = string;
type Value = ... // not important what the actual definition is

type Results = {|
  [key: Key]: Value
|};

const results: Results = {}; // initially empty

But I get the same type error. Should I not be using an exact type for this? I don't want any other types of keys to be allowed in the Results type.

@ksmithbaylor a workaround is to use Shape, as mentioned in https://github.com/facebook/flow/issues/2977#issuecomment-352236384

@ksmithbaylor I hate it myself when somebody responds to a question with "Why don't you use xyz", but why don't you use a Map object for that? :-)

@skovhus Thanks! I ended up using a plain object type (actually with $ReadOnly for my use case) instead of $Shape, because of the issue with $Shape accepting null/undefined as possible values. It turns out Flow doesn't need an exact object type to guarantee that other key/value types don't make it into the object, so my reason for using an exact type in the first place was misplaced.

@lll000111 That was my first instinct too! This is being used in a Redux store, and while a Map would work (and be more semantic), having my state be JSON-serializable without any custom work was something I wanted more. I think something like an ImmutableJS Map would be best here eventually, just not ready to pull it into this project until the need is more obvious.

I'm replacing all of the non-exact types in our codebase with exact and ran into this as well. Since Flow's upcoming change is to make them exact by default, I think it's important to address this issue to make the transition easier.

What it's interesting about this issue is that if you add any prop that it isn't optional, everything works just fine:

// this doesn't work
type State0 = {||}
const state0: State0 = {}

// this doesn't work
type State1 = {| prop0?: number |}
const state1: State1 = {}

// this works
type State2 = {| prop0?: number, prop1: number  |}
const state2: State2 = { prop1: 0 }

@mrkev this definitely is not "feature request"

it's a bug and Object.freeze is just a workaround

flow can have Exact object types where type is exact but object is not frozen at runtime on the other hand Object.freeze will create frozen object at runtime it should be possible to pass empty object where exact empty object is required (same as it's with non empty objects)

https://flow.org/try/#0FAFwngDgpgBA8gIwFZQMYgOoEsQAsByAhgLawC8MA3gD4wDUAdiVAFwwDOIATlgwOYxqAX2DBUAewacYAE0IhCARjaIU6bHiKkYFSjCakWAcgkgGUIzBESpIWfMIAmFcjSYcBZjviv0AOgAzLigoAC8oAAo9A1YTcTMLKwBKUXBoHzV3PHEAVxAtcipaYVEbaTkFAGYXTI1cXPyvXSEAehaYPCx2DnqcgBsZGAB3cS4AaxhCbqGoPr6xSXKHABYatzqGgu9VN0DgsMjKIRSgA

type ObjectWithName = {| +name: string |}

const data1: ObjectWithName = { name:'cotne' }
const data2: ObjectWithName = Object.freeze({ name:'cotne' })

type ObjectWithoutName = {| |}

const data3: ObjectWithoutName = {}// this should work as well
const data4: ObjectWithoutName = Object.freeze({})

name prop is removed from value and type but somehow it broke flow

@TrySound how is this related to "unsealed objects" ?

because it's definitely reports errors with Exact objects.

how else you would declare exact empty object without freezing it at runtime?

@thecotne Empty object is always unsealed. This is the source of the problem here. Isn't it?

@TrySound yes (probably)

flow treats empty objects specially so that you can cast any object to exact object type except empty ones

maybe i don't understand "unsealed objects" label

i was thinking that "unsealed objects" label is for bugs that accrue in unsealed objects (that are actually unsealed and not treat as such)

Empty object is unsealed for the purpose. It's not a bug. Though the purpose makes it unsound in some cases. So I label it as a problem with unsealed objects.

Empty object is unsealed for the purpose.
@TrySound

what is purpose of that?

and is there any option that can disable it? (maybe in strict mode)

I guess for this one. It's often used in facebook AFAIK.
https://flow.org/try/#0MYewdgzgLgBAhjAvDA3gKBjA9F+AuGARjQF800BLAMxgAoAmGAHhgGYBKVDeAOgCMkMegG5SMAKYAbCOK6Y4PYINakgA

I think guys are working on new object model now and will provide a solid solution.

and after that it's not type checked at all right? https://github.com/facebook/flow/issues/2327

i think this should be disabled in strict mode (or in any mode)

it is always possible to mark something as any and disable type checking if that is desired behavior

like this

const a: any = {
  // a: 1
}

if (2 < 3) {
  a.b = 2;
} else {
  a.c = 3
}

works even if object is not empty in the first place

const a: any = {
  a: 1
}

if (2 < 3) {
  a.b = 2;
} else {
  a.c = 3
}

It's still type checked. That's the point. Empty object has more similar use cases as unsealed.
https://flow.org/try/#0MYewdgzgLgBAhjAvDA3gKBjA9F+AuGARjQF800BLAMxgAoAmGAHhgGYBKVDeAOgCMkMegG5SMAKYAbCOK6Y4PYINalyoSLD4FoAJwpgA5oIXAgA

We may bikeshed about it forever. Let's see what guys will propose.

Isn't this a duplicate of #2386?

The only correct workaround for this 4+ years old bug is:
// $FlowFixMe bug: https://github.com/facebook/flow/issues/2977

@FlavienBusseuil Try {...null}

@FlavienBusseuil Try {...null}

@TrySound, I'm not a huge fan of injecting weird syntax to fix identified bugs.

I would much prefer an official fix to remove my // $FlowFixMe. Meanwhile it's much clearer for my teammates than using some dark magics. 🧙

@FlavienBusseuil The thing to be aware of there is that using a FlowFixMe to ignore this is far more dangerous in that it sacrifices type safety entirely, both because it allows for introducing unsealed objects (whose mere existence is a flow bug as far as I'm concerned) and because it just switches off the type system for that line.

For my team, I actually ended up writing a custom auto-fixing eslint rule that we use with eslint-plugin-local-rules:

const unsealedObjectError = () => 'Flow interprets empty objects literals as unsealed objects,' +
  ' see https://git.io/fj64j';

module.exports = {
  'no-unsealed-objects': {
    meta: {
      docs: {
        description: 'no unsealed objects',
        category: 'Possible Errors',
        recommended: false,
      },
      schema: [],
      fixable: 'code',
    },
    create: (context) => {
      // ignore fixture data
      const filename = context.getFilename();
      if (filename.match(/fixtures/)) return { ...null };

      return {
        ObjectExpression: (node) => {
          if (node.properties.length === 0) {
            context.report({
              node,
              message: unsealedObjectError(),
              fix: fixer => fixer.replaceText(node, '{ ...null }'),
            });
          }
        },
      };
    },
  },
};
Was this page helpful?
0 / 5 - 0 ratings