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

[MobX] Cannot decorate undefined property when using makeObservable with babel-preset-typescript #2486

Closed
akphi opened this issue Oct 5, 2020 · 16 comments

Comments

@akphi
Copy link

akphi commented Oct 5, 2020

Hi,

I am trying to upgrade to Mobx@6. This is my before and after class:

// Before
class Person {
  @observable age!: number;
}

// After
class Person {
  age!: number; // EDITED: forgot to add this in the original example

  constructor() {
    makeObservable(this, {
      age: observable
    });
  }
}

When I run the app, the age is not yet initialized, and the error [MobX] Cannot decorate undefined property: 'age' is thrown.

I understand what it means here: based on the doc for makeObservable, only existing property will be made observable. Also I understand that this could be bad typescript usage since I use !: but this is somewhat necessary for our use-case (though I believe we could think of some workaround for this), but more importantly, this used to work fine in mobx@5 so I wonder if this is an expected breaking change in mobx@6?

@mweststrate
Copy link
Member

Yes: https://mobx.js.org/migrating-from-4-or-5.html#getting-started bullet 4 / 5. For background, see https://github.com/mobxjs/mobx/issues?q=Cannot+decorate+undefined+property first couple of hits. So you will need to declare your field explicitly in any case (without TS will complain), and if you don't change your build config, you will have to initialise it as well.

@akphi
Copy link
Author

akphi commented Oct 5, 2020

@mweststrate Sorry I did not search properly :( I read through point 5 and went on to search up that flag in Typescript doc and thought that was a niche TS feature that we didn't have to care about at all :). Now it all makes sense. Thank you so so much!

@mweststrate
Copy link
Member

mweststrate commented Oct 5, 2020 via email

@akphi
Copy link
Author

akphi commented Oct 5, 2020

@mweststrate actually, it doesn't seem to work for my case. I have enabled the options in point 4 and 5.

// tsconfig.json
{
  ...
  "useDefineForClassFields": true
}
// babel.config.js
  ...
  plugins: [
    ['@babel/plugin-proposal-class-properties', { loose: false }],
  ]

Notice the field I had use the type-guard !. I forgot to mention that in the after block, the field age is defined as age!: number. I updated my original example.

@mweststrate
Copy link
Member

probably a config / cache issue somewhere. Above should do the trick. Best create a minimal repo if it doesn't.

@akphi
Copy link
Author

akphi commented Oct 6, 2020

@mweststrate I created a test case for this - https://github.com/akphi/config-tester
While making this, seems like I got a lead. If you look at my webpack.config.js, you will see that test-preset contains the plugin ['@babel/plugin-proposal-class-properties', { loose: false }].

Now if I have this preset running after @babel/preset-typescript the problem emerges. If I have it running before, there will be no problem:

// will have problem with Mobx
    presets: [
      '@babel/preset-env',
      '@babel/preset-react',
      './dev/test-preset', // this now will run after `preset-typescript`
      ['@babel/preset-typescript', {
        onlyRemoveTypeImports: true,
        allowDeclareFields: true
      }],
    ]

// no problem with [Mobx] Can't decorate ..., but will have problem with Typescript `declare` usage in classes
    presets: [
      '@babel/preset-env',
      '@babel/preset-react',
      ['@babel/preset-typescript', {
        onlyRemoveTypeImports: true,
        allowDeclareFields: true
      }],
      './dev/test-preset', // this now will run before `preset-typescript`
    ]

However, I cannot do this since due to my usage of declare for specifying type in sub-class, Typescript compiler will throw if @babel/plugin-transform-typescript runs after @babel/plugin-proposal-class-properties


UPDATED: may relate to this, although our problem is slightly different is that the field is picked up, but it's not initialized. Not too proficient with babel/typescript transpiling debugging myself, but I will try.

@akphi
Copy link
Author

akphi commented Oct 6, 2020

@mweststrate I think I found the root cause. What happened is very nuanced.

  age!: number;

will be treated conceptually the same as declare age: number by @babel/plugin-transform-typescript. So the behavior is to strip this field from class. So when I print out the object before calling makeObservable, I don't even see the field.

But if I do:

  age: number;    // typescript will complain because I don't initialize this, but this will still compile fine

When I print out the object before calling makeObservable, I see the field with value undefined within the object.

Now on niche thing they did was avoiding stripping the field if it was decorated so in that sense, if I do:

  @observable age!: number;

This should work fine, though I would love to get rid of decorators. Now my question is what should I do here:

  1. Should I expect Mobx to adjust to fix this use-case? - I'm not sure what you can do since the field does not even show up in the object
  2. Should I file an issue with babel? - not sure neither since I don't have much deep knowledge on this topic and their discussion seems to be going in the direction of stripping the field with definite assignment operator
  3. Should I opt to use @observable as a workaround until I convert fully to use some kind of factory pattern and only call makeObservable when I know the field has been initialized (I can do this, though it requires some amount of refactoring to be done in my project though).

Sorry if the questions seem a bit scattered, I would come back tomorrow to prune them, but I thought I should give you an update to save time.

@mweststrate
Copy link
Member

Interesting, yes this sounds like an issue in babel; age!: number should define a field. Typescript has a different syntax to only introduce the type for a field without actually defining it: declare age!: number.

Just to reiterate for future readers: A work around should be to assign a value to the age, e.g. age = undefined, or consider setting the field in the constructor before calling makeObservable. As a last resort extendObservable(this, { age: this.age }, { age: observable }) should also do the trick.

@mweststrate mweststrate reopened this Oct 6, 2020
@mweststrate mweststrate changed the title Getting error [MobX] Cannot decorate undefined property when using makeObservable [MobX] Cannot decorate undefined property when using makeObservable with babel-preset-typescript Oct 6, 2020
@mweststrate
Copy link
Member

mweststrate commented Oct 6, 2020

@akphi I still thing it is a problem in your setup, a quick test in the Babel repl translates it correctly link. Please check your config and babel (plugin) versions against the repl.

@akphi
Copy link
Author

akphi commented Oct 6, 2020

@mweststrate possibly, but for the case you just sent, it works because you run plugin-proposal-class-properties before preset-typescript; but that will break declare ... like in this case

@mweststrate
Copy link
Member

mweststrate commented Oct 6, 2020 via email

@akphi
Copy link
Author

akphi commented Oct 6, 2020

yep, that makes sense. I filed an issue over at babel project. Honestly, my use case may not be that popular

Besides what you said, I think another workaround for folks who still have decorator support on is to do this:

class Person {
  @observable age!: number;

  constructor() {
    makeObservable(this, {
      age: observable
    });
  }
}

@mweststrate Should we close this?

@mweststrate
Copy link
Member

Yes, can be closed for now. Thanks for filing that issue!

@akphi
Copy link
Author

akphi commented Oct 15, 2020

Just an update, this problem has been resolved in babel@7.12.0. Everything should work as expected if we follow the migration guide and turning on the flow allowDeclareFields for @babel/preset-typescript

@mweststrate
Copy link
Member

@akphi thanks for the update!

@pie6k
Copy link

pie6k commented Nov 26, 2020

I've had this exact issue as well and it took me way longer than it should to figure it out.

In general, just after class constructor has finished, all properties of the class were becoming undefined.

Example of such class was

class Foo {
  @prop()
  bar!: string;
}

I wanted to see what exactly is removing those props as if I was adding debugger in my constructor, I could see values properly injected.

So I've disabled sourcemaps in chrome and turned out babel is adding something like this just after the constructor:

Object(_Users_project_node_modules_babel_runtime_helpers_esm_initializerDefineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(_this, "email", _descriptor, Object(_Users_project_node_modules_babel_runtime_helpers_esm_assertThisInitialized__WEBPACK_IMPORTED_MODULE_2__["default"])(_this));

the same snippet is added for each prop resulting in them becoming undefined.

How I solved it was 'delaying' initialization code to happen just after the constructor.

so instead of

class Foo {
  constructor() {
    this.init();
  }
  
  init() {}
}

I do

const foo = new Foo();
foo.init();

This way I bypass it and my init code is called after those props are stripped.

I belive this is a bug in babel, as class properties initialization injected by babel should probably check if those props are already initialized by something else during constructor call.

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

3 participants