Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exact type doesn't work with possible empty object #2977

Closed
gcanti opened this issue Dec 8, 2016 · 54 comments
Closed

Exact type doesn't work with possible empty object #2977

gcanti opened this issue Dec 8, 2016 · 54 comments

Comments

@gcanti
Copy link

gcanti commented Dec 8, 2016

Repro

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

Flow version 0.36

@vkurchatkin
Copy link
Contributor

There is the simplest example:

({}:{||})

@vkurchatkin vkurchatkin added the bug label Dec 8, 2016
@minedeljkovic
Copy link

Related #2386

@skovhus
Copy link

skovhus commented Dec 13, 2017

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...

@chrisblossom
Copy link

@skovhus try #4582 (comment) for a workaround.

@skovhus
Copy link

skovhus commented Dec 13, 2017

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

@chrisblossom
Copy link

@skovhus
Copy link

skovhus commented Dec 14, 2017

Thanks @chrisblossom!

@shinout
Copy link
Contributor

shinout commented Dec 17, 2017

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?

@skovhus
Copy link

skovhus commented Dec 17, 2017

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

skovhus added a commit to skovhus/flow-runtime that referenced this issue Jan 7, 2018
Sometimes `{ [any]: empty }` is used as a workaround instead of `{||}`.

See:
- facebook/flow#2977
- facebook/flow#4582
skovhus added a commit to skovhus/flow-runtime that referenced this issue Jan 7, 2018
Sometimes `{ [any]: empty }` is used as a workaround instead of `{||}`.

See:
- facebook/flow#2977
- facebook/flow#4582
@jedwards1211
Copy link
Contributor

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

@skovhus
Copy link

skovhus commented Jan 22, 2018

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

Thanks @shinout! I like this solution better than the [any]: empty trick (#4582 (comment)).

@jedwards1211
Copy link
Contributor

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

@mhagmajer
Copy link

Please also note #5884 when using the $Shape<T> workaround.

@iddan
Copy link

iddan commented Apr 16, 2018

There is no official solution for this yet, right?

@chrisnojima
Copy link

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

@benadamstyles
Copy link

benadamstyles commented Apr 28, 2018

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

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

@mrkev
Copy link
Contributor

mrkev commented May 21, 2018

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

@iddan
Copy link

iddan commented May 21, 2018

Has this been changed with Flow 0.72?

@mrkev mrkev removed the bug label May 21, 2018
@chrisblossom
Copy link

chrisblossom commented May 21, 2018

@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.

@mrkev
Copy link
Contributor

mrkev commented May 22, 2018

@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

@thecotne
Copy link
Contributor

thecotne commented Apr 8, 2019

@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

@thecotne
Copy link
Contributor

thecotne commented Apr 8, 2019

@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?

@TrySound
Copy link
Contributor

TrySound commented Apr 8, 2019

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

@thecotne
Copy link
Contributor

thecotne commented Apr 8, 2019

@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)

@TrySound
Copy link
Contributor

TrySound commented Apr 8, 2019

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.

@thecotne
Copy link
Contributor

thecotne commented Apr 8, 2019

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)

@TrySound
Copy link
Contributor

TrySound commented Apr 8, 2019

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.

@thecotne
Copy link
Contributor

thecotne commented Apr 8, 2019

and after that it's not type checked at all right? #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
}

@TrySound
Copy link
Contributor

TrySound commented Apr 8, 2019

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.

@penx
Copy link
Contributor

penx commented Apr 15, 2020

Isn't this a duplicate of #2386?

@FlavienBusseuil
Copy link

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

@TrySound
Copy link
Contributor

@FlavienBusseuil Try {...null}

@FlavienBusseuil
Copy link

@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. 🧙

@lyleunderwood
Copy link
Contributor

@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 }'),
            });
          }
        },
      };
    },
  },
};

@jedwards1211
Copy link
Contributor

But then to avoid the creeping scourge of wasteful operations to appease flow, you should use a babel plugin to turn { ...null } back into {}...

@jedwards1211
Copy link
Contributor

jedwards1211 commented May 3, 2021

The other problem with $FlowFixMe is that if you later add required properties to the object type, you would want to get an error if you try to assign an empty object, but that error would be suppressed. Right now you have to sacrifice either type safety or idiomatic, optimal code.

@noppa
Copy link
Contributor

noppa commented May 3, 2021

@jedwards1211
Alternatively, you could use Flow's magic comment syntax to do this with zero runtime impact

type A = {|foo?: number|}

const a: A = {/*:: ...null */}

Although this is of course even weirder looking piece of code for the next reader...

@jedwards1211
Copy link
Contributor

jedwards1211 commented May 3, 2021

by using a comment, at least no devs who are less familiar with JS would think there's some obscure runtime purpose to doing { ...null }.
it's kind of like the first time I saw // istanbul ignore next. I had never heard of or used istanbul at the time, and I thought huh? But correctly assumed it was a pragma comment for some tool.

@andidev
Copy link

andidev commented Sep 11, 2021

This

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.

combined with this

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

makes me 😭

Another nasty solution is to do ({}: any) which I am quite used to do now with flow.

@gkz
Copy link
Member

gkz commented Jul 13, 2022

Fixed if you enable the exact_empty_objects=true in your .flowconfig [options] section.

There will be a blog post announcement in the future

@gkz gkz closed this as completed Jul 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests