Description
I am using Typescript 1.8.10. I am using Node v4.4.7 as my primary means of executing Javascript.
Consider this very simple code (I will refer to this later as "code snippet 1"):
class GenConstructFail extends Error {} let x = new GenConstructFail("Test one two"); console.log(x.message)
I run this with tsc test.ts && node test.js
. My expected behavior is that this will print "Test one two". My observed behavior is that it prints only a blank line.
There have been issues filed about this before, the ones I found seem to all link back to #5069 where project member @mhegazy closed the issue and explained "The issue is with how these built in types are hand[led] by the engine. They are extensible as per the es6 spec, but do not think the engines are there yet." However, I believe this is not the correct way to think about the issue.
If I look at the ES2015 spec, I find:
19.5.1 The Error Constructor
The Error constructor is the %Error% intrinsic object and the initial value of the Error property of the global object. When Error is called as a function rather than as a constructor, it creates and initializes a new Error object. Thus the function call Error(…) is equivalent to the object creation expression new Error(…) with the same arguments.
The Error constructor is designed to be subclassable. It may be used as the value of an extends clause of a class definition. Subclass constructors that intend to inherit the specified Error behaviour must include a super call to the Error constructor to create and initialize subclass instances with a [[ErrorData]] internal slot.
The last paragraph here is important. The ES2015 spec requires that Error be subclassable using extends and super(). However, it does not specify Error should be subclassable by other means. Here is what tsc
emits when run with the default target of ES5 (I will refer to this later as "code snippet 2"):
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 GenConstructFail = (function (_super) {
__extends(GenConstructFail, _super);
function GenConstructFail(m) {
_super.call(this, m);
}
return GenConstructFail;
}(Error));
var x = new GenConstructFail("Test one two");
console.log(x.message);
I do not fully understand what the Typescript __extends
function is doing, but the generated code for GenConstructCall
appears to revolve around expecting it can invoke call
on the _super
object, in this case Error
. However, call
is a method on Function.prototype
and Error
does not have Function.prototype
in its prototype chain. Error
is an intrinsic object, its prototype is also an intrinsic object (defined in section 19.5.2.1 of the spec), and the operations supported on Error
, its constructor, and its prototype are all explicitly enumerated. call
is not among them.
So in other words, if the code Typescript generates in ES5 mode does not behave correctly with regard to constructor parameters, it is not because the engines have not yet caught up to ES6, it is because Typescript has generated code which is neither correct ES5 nor correct ES6.
In fact, in my testing, I find that ES6 engines that fully support "code snippet 1" above when simply evaluated as code, do not support "code snippet 2" (the Typescript-transpiled-to-ES5 version of the same code). I tested with Chrome version "51.0.2704.106 m" (up to date) and also with "Microsoft Edge 25.10586.0.0 / Microsoft EdgeHTML 13.10586"; in both cases, code snippet 1 printed "Test one two" and code snippet 2 printed a blank line. (A person I talked to on Twitter tested with Node 5.4.1 and saw these same results.) I also found that in both Chrome and Edge, Error("test")
produced an Error object with the message "test" (this is a special behavior guaranteed by section 19.5.1.1 of the ES2015 spec) but Error.call("test")
produced an Error object with a blank message.
So, to summarize: Typescript's ES5 target generates an invalid constructor when Error
is extended, it is doing this predictably and 100% of the time, and it is invalid because of the Javascript spec and not because of any particular engine limitation. This should be considered a bug.
Moreover, it seems Typescript could be easily modified to fix this. The problem is that .call
invocation. Consider this alternate version of code snippet 1 (let's call this "code snippet 3"):
class GenConstructFail extends Error { constructor(m:string) { super(); this.message = m } }
let x = new GenConstructFail("TEST THREE FOUR"); console.log(x.message)
Here is the Javascript tsc
emits for code snippet 3 (let's call this "code snippet 4"):
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 GenConstructFail = (function (_super) {
__extends(GenConstructFail, _super);
function GenConstructFail(m) {
_super.call(this);
this.message = m;
}
return GenConstructFail;
}(Error));
var x = new GenConstructFail("TEST THREE FOUR");
console.log(x.message);
The problem with "code snippet 2", at least with V8 and Edge, is that call()
is swallowing the arguments to the constructor (which is after all not a real constructor). Assigning the fields after the call()
avoids this problem and the code prints "TEST THREE FOUR" even on my old copy of Node 4.4.7.
My suggested fix is that Typescript should detect when it is subclassing Error
or one of the built-in Error
subclasses (all intrinsics) while in ES5 mode, and in this case generate a constructor which instead of assuming call()
works simply takes the arguments to super()
and assigns them to this
after call()
. (And maybe it would be better to find a way to avoid call()
altogether if you can, since I see nothing in the spec to guarantee it even exists on Error
.)