-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Incorrect transpiled constructor of derived classes #12123
Comments
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 |
@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. |
extending Error is quite common in JavaScript land. This would break many libraries, say Angular2.
|
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. |
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. |
but ignore some pointless refactoring as 34c02a PS: duo to microsoft/TypeScript#12123, 9 testcases don't pass test. But it will pass with future typescript
@falsandtru I found another issue. class A {
constructor() {
return {}
}
}
class B extends A {}
var b = new B()
b instanceof A // should be false Manually setting 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. class A {
constructor() {
return 123
}
}
class B extends A {
constructor() {
super()
this['prop'] = 13
}
}
var b = new B()
console.log(b['prop']) // tsc prints undefined, node prints 13 As a comparison, Babel will log The most proper way I can conceive is
@DanielRosenwasser What's your opinion? |
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 class A {
constructor() {
return 1
}
}
console.log(new A()) // node prints A, not 1 |
Returning primitive is supported in JS spec. Babel supports this by Primitive value will only influence inherited class. In the example above. TS also prints A because And inheriting is also fine before #10762 because TS used to ignore super constructor's return value. Now, for code like |
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? |
The rule of thumb is: The first rule is specified by 9.2.2 in ECMA262.
So, even if A return a number, The second rule applies to transpiled body. var A = (function () {
function A() {
return 123;
}
return A;
}());
var B = (function (_super) {
__extends(B, _super);
function B() {
var _this = _super.apply(this, arguments) || this; // here, _super.apply(this, arguments) return 123
_this.prop = 123;
return _this; // _this is primitive value 1232
}
return B;
}(A)); In the transpiled js, _super.apply will return 123, so B's constructor will return primitive. Then rule 1 comes to play, which makes 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. |
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. |
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: 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;
// do something
if (_that) {
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); // true |
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
Does this mean code below is not permitted? class A {
constructor() { return this; }
}
class B extends A {} |
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 add: without when |
@ahejlsberg @sandersn @vladima @RyanCavanaugh @DanielRosenwasser @mhegazy Can you resolve these problems before releasing 2.1? I believe TypeScript must not publish these problems via stable versions. At least these problems will break some code because of missing subclass methods. |
The thing is that extending @falsandtru can you elaborate on the issue you mentioned when inheriting two levels deep or more? |
@HerringtonDarkholme what is your use case for returning a primitive type from the constructor of your class? |
@DanielRosenwasser Handling primitive return value is not for real use but for bug catching. If some one return value in constructor from external sources like server call, it will misbehave, of course. But the point here is the misbehavior in TypeScript should also follow ES spec. |
As I said in #12123 (comment), this problem is not restricted to builtin objects. All subclasses which return a truthy value lose their methods. It will affect not a few users. So it need to fix natively. About "two levels", I found the unexpected generated return statements only in subclasses. However, I don't investigate the details yet. |
@falsandtru Yes, they should lose methods. In node 6: class A {constructor() {return {}}};
(new A) instanceof A // false |
Sorry, I confused about the two different cases. class B {
constructor() {
return {};
}
}
class D extends B {
m() {
}
}
console.log(new D().m); // undefined, this method is lost possibly correctly. class D extends Error {
m() {
}
}
console.log(new D().m); // undefined, this method is lost incorrectly. So I take back my words. This problem is restricted to a few cases such as builtin objects. |
It is worth noting that in say, recent versions of chrome, extending an ES6 class like Now code compiled for older browsers breaks on newer browsers. I guess this is a chicken and egg problem because ES6 uses I realize that this is tangentially related at best... but I thought it worth bringing up. |
@aluanhaddad Also see #11304. |
Merge into #10166 |
see microsoft/TypeScript#12123 for more details
It's solution doesn't not work on TypeScript v2.7.2:
Compile error:
|
My solution: tsconfig.json:
custom-error.ts:
|
My dudes. Have you heard the good news about factory functions? function CustomError<T extends object>(
message: string,
properties: T,
ctor: Function = CustomError
): Error & T {
const error = Error(message)
Error.captureStackTrace(error, ctor)
return Object.assign(error, { name: ctor.name }, properties)
}
interface InvalidFoo extends Error {
code: 1000
data: { foo: string }
}
function InvalidFoo(data: InvalidFoo['data']): InvalidFoo {
return CustomError(`Invalid foo: ${foo}`, { code: 1000, data: { foo } }, InvalidFoo)
} |
My solution here
CustomErrorexport interface IErrorConstructor {
new(...args: any[]): Error;
}
// tslint:disable:max-line-length
/**
* Workaround for custom errors when compiling typescript targeting 'ES5'.
* see: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
* @param {CustomError} error
* @param newTarget the value of `new.target`
* @param {Function} errorType
*/
// tslint:enable:max-line-length
export function fixError(error: Error, newTarget: IErrorConstructor, errorType: IErrorConstructor) {
Object.setPrototypeOf(error, errorType.prototype);
// when an error constructor is invoked with the `new` operator
if (newTarget === errorType) {
error.name = newTarget.name;
// exclude the constructor call of the error type from the stack trace.
if (Error.captureStackTrace) {
Error.captureStackTrace(error, errorType);
} else {
const stack = new Error(error.message).stack;
if (stack) {
error.stack = fixStack(stack, `new ${newTarget.name}`);
}
}
}
}
export function fixStack(stack: string, functionName: string) {
if (!stack) return stack;
if (!functionName) return stack;
// exclude lines starts with: " at functionName "
const exclusion: RegExp = new RegExp(`\\s+at\\s${functionName}\\s`);
const lines = stack.split('\n');
const resultLines = lines.filter((line) => !line.match(exclusion));
return resultLines.join('\n');
}
export class CustomError extends Error {
constructor(message: string) {
super(message);
fixError(this, new.target, CustomError);
}
} unit tests
|
@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. |
The assert(e instanceof InvariantError) test is currently broken due to these outstanding TypeScript bugs: microsoft/TypeScript#13965 microsoft/TypeScript#12123 microsoft/TypeScript#12790
Extending the built-in `Error` class in TypeScript results in a subclass whose `.prototype` is simply `Error.prototype`, rather than (in this case) `WriteError.prototype`, so this code never worked as intended, because `new WriteError(message) instanceof WriteError` would never be true. Relevant discussion (content warning: maintainers refusing to fix deviations from the ECMAScript specification for reasons that are not even remotely compelling): microsoft/TypeScript#12123 microsoft/TypeScript#12581 More subjectively (IMHO), the additional messaging did not ease debugging enough to justify its contribution to bundle sizes.
Extending the built-in `Error` class in TypeScript results in a subclass whose `.prototype` is simply `Error.prototype`, rather than (in this case) `WriteError.prototype`, so this code never worked as intended, because `new WriteError(message) instanceof WriteError` would never be true. Relevant discussion (content warning: maintainers refusing to fix deviations from the ECMAScript specification for reasons that are not even remotely compelling): microsoft/TypeScript#12123 microsoft/TypeScript#12581 More subjectively (IMHO), the additional messaging did not ease debugging enough to justify its contribution to bundle sizes.
TypeScript Version: master
Code
Expected behavior:
Actual behavior:
The text was updated successfully, but these errors were encountered: