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

TypeScript: Action-annotated method is undefined #1790

Closed
jbreckmckye opened this issue Oct 26, 2018 · 11 comments
Closed

TypeScript: Action-annotated method is undefined #1790

jbreckmckye opened this issue Oct 26, 2018 · 11 comments

Comments

@jbreckmckye
Copy link

jbreckmckye commented Oct 26, 2018

I'm having a strange issue with the MobX annotations, where an @action method doesn't exist on the resultant object. I'm not sure if the issue is with MobX, TypeScript / TSC, Parcel, or something else.

If my class source is the following TypeScript:

    export class Car {
        @observable
        public wheels: number = 4;
    
        @action
        public selfDestruct() {
            this.wheels = 0;
        }
    }

And I invoke the method as follows:

    const car = new Car();
    car.selfDestruct();

I get an error:

Uncaught TypeError: car.selfDestruct is not a function

Evaluating car.selfDestruct in the console returns undefined.

However, if I use the action function all seems fine:

    export class Car {
        @observable
        public wheels: number = 4;
    
        public selfDestruct = action(
            () => this.wheels = 0
        );
    }
    
    const car = new Car();
    
    car.selfDestruct(); // works fine

Why can't I use the @action annotation?

For reference, I am using MobX 5.5.2 with TypeScript 3.1.1. The compilation is being handled by ParcelJS 1.10.1. The target is an Electron app.


if it helps, here's my tsconfig:

{
    "compilerOptions": {
        // Compiled module type. Node uses CommonJS.
        "module": "commonjs",

        // Target language level
        "target": "es2016",

        // Explodes if type cannot be inferred
        "noImplicitAny": true,

        // Don't assume any type may also be a null
        "strictNullChecks": true,

        // Follow module paths like a Node app
        "moduleResolution": "node",

        // Where to output compiled code
        "outDir": "dist",

        // Allow ES2015-style default imports from files with no explicit export-default
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,

        "experimentalDecorators": true,

        // Transpile JSX files to React-Hyperscript
        "jsx": "react"
    },
    "exclude": ["node_modules"]
}
@chutchinson
Copy link

I fixed this by switching the compilation target to "es5". Haven't looked into the underlying problem.

@jbreckmckye
Copy link
Author

If I target ES5, won't I lose the benefits of proxies?

@mweststrate
Copy link
Member

@jbreckmckye no, target just specifies which language features are used, not which standard library is used

@jbreckmckye
Copy link
Author

jbreckmckye commented Nov 6, 2018

It's still not working for me unfortunately.

Also - I can't get class members decorated with @observable to function either.

For example, if I instantiate the class -

class Foo {
     @observable
     public myNumber = 5;
}

foo = new Foo();

When I inspect the object, the myNumber property is not an observable, but a raw primitive:

image

This is using the TSC options target: es5 and lib: ['dom', 'es2015']

@jbreckmckye
Copy link
Author

jbreckmckye commented Nov 6, 2018

Oh - one thing, I am using Parcel's --target electron flag, which (as I understand it) means the bundle won't contain any of its package.json dependencies - they'll get imported at runtime instead. Could this affect anything?

@mweststrate
Copy link
Member

mweststrate commented Nov 6, 2018 via email

@jbreckmckye
Copy link
Author

jbreckmckye commented Nov 6, 2018

Hi Michael - thanks for being so patient.

So - this is going to sound rather strange, but I think I've found the issue, and it's something to do with my tsconfig setup, with multiple config files.


I originally had three tsconfig files - one root, one for the main thread and one for the renderer, as below:

main tsconfig

{
    "compilerOptions": {
        // Compiled module type. Node uses CommonJS.
        "module": "commonjs",

        // Target language level
        "target": "es2016",

        // Explodes if type cannot be inferred
        "noImplicitAny": true,

        // Don't assume any type may also be a null
        "strictNullChecks": true,

        // Follow module paths like a Node app
        "moduleResolution": "node",

        // Where to output compiled code
        "outDir": "dist",

        // Allow ES2015-style default imports from files with no explicit export-default
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true
    },
    "exclude": ["node_modules"]
}

renderer tsconfig:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "jsx": "react",
    "experimentalDecorators": true
  },
  "include": ["./**/*", "../types/**/*"]
}

The TypeScript documentation suggests that this should work just fine; all the compilerOptions will be merged for each compile target.

This would output class code that looked a little like this:

function () {
  function SomeClass(imagery, record) {
      ...
  }

  Project.prototype.doAThing = function () {
    console.log('this is an action');
  };
  __decorate([mobx_1.action], Project.prototype, "doAThing");

  return Project;
}();

Calling new Someclass().doAThing with this setup results in an undefined is not a function.

When I join the tsconfigs, however, things look subtly different. The proper class syntax is used and the __decorate function is called in a different way:

unified tsconfig

{
    "compilerOptions": {
        // Compiled module type. Node uses CommonJS.
        "module": "commonjs",

        // Target language level
        "target": "es2016",

        // Explodes if type cannot be inferred
        "noImplicitAny": true,

        // Don't assume any type may also be a null
        "strictNullChecks": true,

        // Follow module paths like a Node app
        "moduleResolution": "node",

        // Where to output compiled code
        "outDir": "dist",

        // Allow ES2015-style default imports from files with no explicit export-default
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,

        "experimentalDecorators": true,

        // Transpile JSX files to React-Hyperscript
        "jsx": "react"
    },
    "include": [
        "./src/**/*"
    ],
    "exclude": ["node_modules"]
}

Generated code:

class Project {
  constructor(imagery, record) {...}

  doAThing() {
    console.log('this is an action');
  }
}
__decorate([mobx_1.action], Project.prototype, "doAThing", null);

Now, both @observable and @action annotations seem to work just fine.


It's not clear to me exactly why my previous setup was mangling the compile output. Either the compilerOptions aren't really being merged properly or something strange is happening under the covers of tsc.

In either case, it seems I solve the issue fairly easily by using a single tsconfig.json file. This may also solve some IDE problems I'm having.

I'm not sure if there's a clear takeaway or explanation as to what can go wrong with multiple tsconfigs, but at least I now know they have some pitfalls. Thanks for your patience on this.

@jonlambert
Copy link

jonlambert commented Dec 8, 2018

I'm experiencing this issue too. I'm using MobX with TypeScript and Next.js. @action or @observable decorated attributes are all initially undefined.

import { observable } from 'mobx';

class UserStore {
  @observable name = "Jon";
}

const store = new UserStore();

console.log(store.name); // undefined

Unfortunately changing the target to es5 doesn't help, and having to unify the tsconfig.json file seems like an unusual solution, especially as I only have one. Does anybody know why this might be happening?

@mweststrate
Copy link
Member

mweststrate commented Dec 8, 2018 via email

@jonlambert
Copy link

For those finding this issue later I managed to solve it by ensuring @babel/plugin-proposal-decorators is the first plugin in the list.

{
  "presets": ["next/babel", "@zeit/next-typescript/babel"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    "transform-class-properties",
  ]
}

@lock
Copy link

lock bot commented Jul 21, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions.

@lock lock bot locked as resolved and limited conversation to collaborators Jul 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants