Typescript: unexpected transform order of public class fields

Created on 2 Oct 2019  路  8Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.6.3


Search Terms: public class field

Code

// code from https://2ality.com/2019/07/public-class-fields.html
class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('Before super()');
    super()
    console.log('sub-constructor');
  }
}
new SubClass();

Expected behavior:
The above code emits the following ES6 code:

class SuperClass {
    constructor() {
        this.superProp = console.log('superProp');
        console.log('super-constructor');
    }
}
class SubClass extends SuperClass {
    constructor() {
        this.subProp = console.log('subProp');
        console.log('Before super()');
        super();
        console.log('sub-constructor');
    }
}
new SubClass();

this.subProp = console.log('subProp') is defined before super() called, which will throw if we run it. playground link

If target ES5, it emits:

...
...
var SuperClass = /** @class */ (function () {
    function SuperClass() {
        this.superProp = console.log('superProp');
        console.log('super-constructor');
    }
    return SuperClass;
}());
var SubClass = /** @class */ (function (_super) {
    __extends(SubClass, _super);
    function SubClass() {
        var _this = this;
        _this.subProp = console.log('subProp');  // this line should be placed after the super() call.
        console.log('Before super()');
        _this = _super.call(this) || this;
        console.log('sub-constructor');
        return _this;
    }
    return SubClass;
}(SuperClass));
new SubClass();

the code will print:

subProp
Before super()
superProp
super-constructor
sub-constructor

According to the proposal class fields :

When field initializers are evaluated and fields are added to instances:
Base class: At the beginning of the constructor execution, even before parameter destructuring.
Derived class: Right after super() returns.

The expected results should be:

Before super()
superProp
super-constructor
subProp
sub-constructor

Besides, the emitted code defines fields on instances with [[Set]] sematics this.subProp = console.log('subProp'), instead of the expected [[Define]] semantics Object.defineProperty(SubClass, "subProp", { value: console.log('subProp'), ... })

Actual behavior:
emittd code unexpectedly prints:

subProp
Before super()
superProp
super-constructor
sub-constructor

Related Issues:

Duplicate

Most helpful comment

In a base class that has field initializers, TypeScript currently requires the first line of the constructor to be super(). This is a restriction of TypeScript, not of JavaScript. If you violate this rule, the emitted code is incorrect even if you silence the checker.

Here's a playground with a minimal repro.

Input:

class Base {}
class Sub extends Base {
    // @ts-ignore
    constructor() {
        console.log('hi');
        super();
    }
    field = 0;
}
new Sub();

Emitted:

class Base {}
class Sub extends Base {
    // @ts-ignore
    constructor() {
        this.field = 0;
        console.log('hi');
        super();
    }
}
new Sub();

In my opinion, this is a bug that should be fixed. And it would also help bolster the claim that TS is a superset of JS 馃槈

All 8 comments

Besides, the emitted code defines fields on instances with [[Set]] sematics this.subProp = console.log('subProp'), instead of the expected [[Define]] semantics

I think that's intentional:
https://github.com/microsoft/TypeScript/pull/33509

In 3.7, "useDefineForClassFields" defaults to false

Was this duplicated?
I mentioned two problems in this issue.

33509 just fixes part of it.

Will that fix the code transform order ?

@Meowu can you be more clear about what the other problem is?

As I show in the above code, after transformed,
if target ES6:

class SubClass extends SuperClass {
    constructor() {
        this.subProp = console.log('subProp');  //  'this' referred before super() call. 
        console.log('Before super()');
        super();
        console.log('sub-constructor');
    }
}

It will throw error.

if targer ES5:

...
var SubClass = /** @class */ (function (_super) {
    __extends(SubClass, _super);
    function SubClass() {
        var _this = this;
        _this.subProp = console.log('subProp');  // this line should be placed after the super() call.
        console.log('Before super()');
        _this = _super.call(this) || this;
        console.log('sub-constructor');
        return _this;
    }
    return SubClass;
}(SuperClass));
...

Although this won't throw, the 'console.log' invokded in a unexpected order. 'subProp' will be printed first.

The code has an error; unexpected results in the presence of errors are... expected

Sorry, I didn't paste the full code.
Below is the raw code.

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('Before super()');
    super()
    console.log('sub-constructor');
  }
}
new SubClass();

after transformed:

// target ES6
class SubClass extends SuperClass {
    constructor() {
        this.subProp = console.log('subProp');  //  'this' referred before super() call. 
        console.log('Before super()');
        super();
        console.log('sub-constructor');
    }
}

// target ES5
...
var SubClass = /** @class */ (function (_super) {
    __extends(SubClass, _super);
    function SubClass() {
        var _this = this;
        _this.subProp = console.log('subProp');  // this line should be placed after the super() call.
        console.log('Before super()');
        _this = _super.call(this) || this;
        console.log('sub-constructor');
        return _this;
    }
    return SubClass;
}(SuperClass));
...

I transformed the same code with babel, it did the right thing.

In a base class that has field initializers, TypeScript currently requires the first line of the constructor to be super(). This is a restriction of TypeScript, not of JavaScript. If you violate this rule, the emitted code is incorrect even if you silence the checker.

Here's a playground with a minimal repro.

Input:

class Base {}
class Sub extends Base {
    // @ts-ignore
    constructor() {
        console.log('hi');
        super();
    }
    field = 0;
}
new Sub();

Emitted:

class Base {}
class Sub extends Base {
    // @ts-ignore
    constructor() {
        this.field = 0;
        console.log('hi');
        super();
    }
}
new Sub();

In my opinion, this is a bug that should be fixed. And it would also help bolster the claim that TS is a superset of JS 馃槈

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Was this page helpful?
0 / 5 - 0 ratings