Repository navigation
Incorrect transpiled constructor of derived classes #12123
Copy link
Copy link
Closed
Labels
Breaking ChangeWould introduce errors in existing codeWould introduce errors in existing codeCanonicalThis issue contains a lengthy and complete description of a particular problem, solution, or designThis issue contains a lengthy and complete description of a particular problem, solution, or designDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
Milestone
Description
- Missing own class methods
- Missing relationship with own class (Extending from Error doesn't work when emitting ES5 #10166)
TypeScript Version: master
Code
class C extends Error {
constructor() {
super('error');
}
m() {
}
}
console.log(new C().message);
console.log(new C().m);
console.log(new C() instanceof Error);
console.log(new C() instanceof C);Expected behavior:
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var C = (function (_super) {
__extends(C, _super);
function C() {
var _that = _super.call(this, 'error');
if (_that) {
_that.__proto__ = this;
}
var _this = _that || this;
return _this;
}
C.prototype.m = function () {
};
return C;
}(Error));
console.log(new C().message); // 'error'
console.log(new C().m); // [Function]
console.log(new C() instanceof Error); // true
console.log(new C() instanceof C); // trueActual behavior:
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var C = (function (_super) {
__extends(C, _super);
function C() {
return _super.call(this, 'error') || this;
}
C.prototype.m = function () {
};
return C;
}(Error));
console.log(new C().message); // 'error'
console.log(new C().m); // undefined
console.log(new C() instanceof Error); // true
console.log(new C() instanceof C); // falseReactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Breaking ChangeWould introduce errors in existing codeWould introduce errors in existing codeCanonicalThis issue contains a lengthy and complete description of a particular problem, solution, or designThis issue contains a lengthy and complete description of a particular problem, solution, or designDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
Activity
HerringtonDarkholme commentedon Nov 9, 2016
Babel has the same behavior... babel/babel#3083
And babel only supports extending builtin object as option plugin. https://www.npmjs.com/package/babel-plugin-transform-builtin-extend
Buth babel's fix is quite problematic as
__proto__is not universally supported and is specified as legacy feature.e-cloud commentedon Nov 9, 2016
falsandtru (@falsandtru), one thing should be mentioned is that your expected behavior will generate a weird prototype chain.
However, since it's hard to be fully compatible. That's acceptable temporarily.
HerringtonDarkholme commentedon Nov 9, 2016
extending Error is quite common in JavaScript land. This would break many libraries, say Angular2.
https://github.com/angular/angular/blob/745e10e6d2ea9097b7ec650ae54cea91e3d193f2/modules/%40angular/facade/src/errors.ts#L16
It turns out Angular also have special handling for property lost.And would be very hard to debug and discover. Maybe we can just list all native constructor to skip emitting
var _this = _super.call(this, 'error') || this.falsandtru commentedon Nov 9, 2016
I think so, but this solution possibly has no regression. So TypeScript can fix this bug using this solution. Additionally, the latest stable version doesn't have this bug. So this bug will be the large regression, and it is too large to release as a stable version.
falsandtru commentedon Nov 9, 2016
Ah, not true. Probably there is no regression for 2.1.0 (RC), but this solution makes some regressions for 2.0.8 (stable) at least on IE10 or below.
update: sync with merge changes from last commit
HerringtonDarkholme commentedon Nov 10, 2016
falsandtru (@falsandtru) I found another issue.
Manually setting
__proto__is still problematic when returning a non primitive value.Also, the current behavior still have another problem that when a primitive value is return, subclass properties are not correctly set. The following example can be run in both node and tsc. But their results are different.
As a comparison, Babel will log
b['prop']as 13.The most proper way I can conceive is
_super.apply(this, arguments), assign it to_thisiff. it has type ofobjectorfunctionErrorandArray,_thisshould always bethisDaniel Rosenwasser (@DanielRosenwasser) What's your opinion?
falsandtru commentedon Nov 10, 2016
Of course, this issue is not restricted to builtin objects. But probably we don't need to consider primitive values now because seems like it is not supported by the spec. However, if TS users incorrectly use primitive values, the current code like
_super.call(this, ...) || thiswill make some different behaviors.HerringtonDarkholme commentedon Nov 10, 2016
Returning primitive is supported in JS spec. Babel supports this by
_possibleConstructorReturnhelper.Primitive value will only influence inherited class. In the example above. TS also prints A because
new Awill returnthisinstead if the return value is primitive. Spec here.And inheriting is also fine before #10762 because TS used to ignore super constructor's return value.
Now, for code like
class A {constructor() {return 123}}; class B extends A {},new Bwill still returnthisobject because_super.call(this, ...) || thisevaluates to a primitive. Yet additional operations in child class constructor is lost.falsandtru commentedon Nov 10, 2016
Thanks for the info, but I did run my previous example without transpiling. If you are right, why I cannot to get the primitive value result in that example?
HerringtonDarkholme commentedon Nov 10, 2016
The rule of thumb is:
new Ctorwill never get a primitive return value. ButCtor.apply(this, arguments)can return a primitive value.The first rule is specified by 9.2.2 in ECMA262.
So, even if A return a number,
new A()will return this object.The second rule applies to transpiled body.
In the transpiled js, _super.apply will return 123, so B's constructor will return primitive. Then rule 1 comes to play, which makes
new Bathisobject wherepropis never assigned.This example might be contrived. But there might non trivial code return value that relies on external data (a http call, a database access). In those scenarios this bug might be very hard to find.
falsandtru commentedon Nov 10, 2016
I understand, it is one of the case. But I think the handling of primitives is not very important in this issue. It can be included, but it can be also a separated issue. So I didn't include it in this issue's description.
falsandtru commentedon Nov 10, 2016
As a note, I found another bug. Compiler must not create return statements oneself. It makes another bug when inheriting 2 times or more. So a more expected code is:
HerringtonDarkholme commentedon Nov 10, 2016
I really appreciate your effort to couple with extending native object. But it turns out very very hard for transpiler. For example, Babel does not support native Error at all! Angular2 uses custom getter/setter to mock native behavior.
Setting proto to
thisis cyclic, which, I would argue, introduces more problem than it solves.Does this mean code below is not permitted?
falsandtru commentedon Nov 10, 2016
No, I don't block any source code. I mean generated return statements which are not written in source code have an unexpected influence on
_super.call(this, ...) || this.add: without when
_super.call(this, ...)returns any object. Transpiled class constructor needs to return this object. Then compiler needs to add return statements oneself as I wrote the example code in the previous comment #12123 (comment).21 remaining items
falsandtru commentedon Dec 7, 2016
Great!
fix: test error due to Typescript limitation
devpreview commentedon Feb 23, 2018
Daniel Rosenwasser (@DanielRosenwasser)
It's solution doesn't not work on TypeScript v2.7.2:
Compile error:
devpreview commentedon Feb 23, 2018
My solution:
tsconfig.json:
custom-error.ts:
christianbradley commentedon Mar 9, 2018
My dudes. Have you heard the good news about factory functions?
Yaojian commentedon Jun 6, 2018
My solution here
CustomError
unit tests
TomaszWaszczyk commentedon Sep 19, 2018
Mohamed Hegazy (@mhegazy) Could you describe what you mean by "alternatively, you can just set this.proto instead of using Object.setPrototypeOf`". I am facing the issue having TypeScript 3.0.3.
Add ts-invariant package.
Work around TypeScript's failure to extend Error correctly.
Remove unhelpful and slightly buggy enhanceErrorWithDocument function.
Remove unhelpful and slightly buggy enhanceErrorWithDocument function.