Ember 3.2 + ember-decorators subclassing bug

While migrating a 3.1.4 project to 3.2, I ran into the following error:

Class constructor A cannot be invoked without 'new'.

The project has 2 models, A and B, defined with ES6 class/extends syntax thanks to ember-decorators.

// models/a.js
import DS from 'ember-data';

export default class A extends DS.Model {
}
// models/b.js
import A from './a';

// I need A.extend() here for polymorphic relationship reasons
export default class B extends A.extend() {
}

This code works under 3.1.4 but fails under 3.2 and I do not understand why.

The workaround I’ve found is to define A with EmberObject.extend() instead of using ES6 class syntax.

// models/a.js
import DS from 'ember-data';

// this works under 3.1 and 3.2
export default DS.Model.extend({});

Another way to fix the issue is to define B like this

// models/b.js
import A from './a';

// no more A.extend(), so no more bug, but it does not fit my needs.
export default class B extends A {
}

I’ve setup a repo to reproduce the bug:

I’d love to understand what’s going on here and how I could migrate to 3.2 without refactoring A to use EmberObject.extend :wink:

I took a few minutes and checked things out in your demo repo (thanks for putting that together!), here is a relative brain dump of what is going on here.


In Ember 3.2, the internals of .extend() were changed to better support ES class interoperability. This was specifically intended to fix the exact scenarios that you are running into, and we have tests to confirm this back and forth (.extend() → class extends → .extend() → class extends ) works properly both with and without transpilation!

Unfortunately, the way Ember is distributed (as a pre-transpiled global bundle) means that we ended up in a scenario that we didn’t test or plan for :sob:. The fundamental issue is that the class interop code that was done in Ember 3.2+ ends up being transpiled away by babel (since Ember is transpiled to support IE11), and instead of the Ember object base class calling super as appropriate it is using the transpiled version (BaseConstructor.call(this)) which causes errors.

Also, to be clear the errors being thrown are unrelated to ember-decorators or ember-data. The following also reproduces the issue:

class Foo extends Ember.Object {}
class Bar extends Foo.extend() {}

Bar.create();

To work around the issue for now, you can change your code to ensure that you do not call .extend() on anything created by class extends. The example above would look like:

class Foo extends Ember.Object {}
class Bar extends Foo {}

Bar.create();

The real fix will have to be made in Ember itself where we need to change things to ensure that Ember is transpiled in the same way as your app code.

Hope all this makes sense, and I’m sorry for the issue :crying_cat_face:.

1 Like

Thanks a lot for taking the time to investigate and to write down this answer! All this is very enlightening.

Since this seems to be a bug in Ember itself, do you need me to file an issue or have you done this already?

As far as my project is concerned, I cannot work around this by removing .extend() because I need it for a polymorphic hasMany relationship. See my other post about this: Polymorphic hasMany relathionship with ES6 classes.

Others might also need .extend() to include mixins. I guess the right solution for the time being is to rewrite the super class without the ES6 class syntax;

@bartocc, @rwjblue - was this ever fixed? I just hit the same issue on Ember 3.5.0. We have a:

export default class Model extends EmberObject {

And then we do:

let x = Model.extend(...mixins, properties)

And then calling “new x()” blows up with the same error. Is there an issue on the issue-tracker I can subscribe to? Was this fixed? Did it break again?

1 Like

I suggested a workaround here https://github.com/emberjs/ember.js/issues/17042#issuecomment-444724346

If in your app you want to just apply mixins to your native class you could do

export default class Model extends EmberObject {}
Model.reopen(...mixins, properties);

which is the same as extend() but now babel will transpile the class extends the same as your app, so if babel-preset-env supports your target it will keep it.

Another way that works is export default class Model extends EmberObject.extends(...mixins, properties)

try this:

// ember-cli-build.js

let app = new EmberApp(defaults, {
	// ...

    babel: {
      include: [
        'transform-classes',
      ],
    },
	// ...