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?
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.
@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!
Np!
On Mon, 5 Oct 2020, 21:34 An Phi, notifications@github.com wrote:
@mweststrate https://github.com/mweststrate Sorry I did not search
properly :( I read through point 5 and went on to search up that flag in
Typescript doc and though that was not a feature we care about at all :).
Now it all makes sense. Thank you so so much!—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/2486#issuecomment-703873161, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN4NBHQHKKTDZ5RJZLBMCLSJIUUNANCNFSM4SFEJVHQ
.
@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.
probably a config / cache issue somewhere. Above should do the trick. Best create a minimal repo if it doesn't.
@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.
@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:
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@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.
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.
@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.
@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
Ok, then best file an issue with Babel :)
On Tue, Oct 6, 2020 at 9:37 AM An Phi notifications@github.com wrote:
@mweststrate https://github.com/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
https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&spec=true&loose=false&code_lz=MYGwhgzhAEAqCmEAu0DeAoaXoBN6jACd5owBzeALmgDsBXAWwCN5CBuTASADMBLeEDgCE1es1Zto6AL5A&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Cstage-3%2Ctypescript%2Cenv&prettier=false&targets=&version=7.11.6&externalPlugins=%40babel%2Fplugin-proposal-class-properties%407.10.4—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/2486#issuecomment-704120454, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN4NBH2MTZRZI4SSZ43URDSJLJNRANCNFSM4SFEJVHQ
.
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?
Yes, can be closed for now. Thanks for filing that issue!
Just an update, this problem has been resolved in [email protected]. Everything should work as expected if we follow the migration guide and turning on the flow allowDeclareFields for @babel/preset-typescript
@akphi thanks for the update!
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.
Most helpful comment
Just an update, this problem has been resolved in
[email protected]. Everything should work as expected if we follow the migration guide and turning on the flowallowDeclareFieldsfor@babel/preset-typescript