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:
Besides, the emitted code defines fields on instances with
[[Set]]sematicsthis.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.
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.
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:
Emitted:
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 馃槈