通过 Typescript class 继承 Error 实现自定义错误类型并编译到 ES5 时,遇到了一个坑。

class MyError extends Error {}

compilerOptions.target 设为 "es5"。

但是运行起来:

const err = new MyError()
err instanceof Error // true
err instanceof MyError // 结果竟然是 false

原因

使用 Babel/Typescript 编译出的代码有类似的问题

Typescript 2.7.2 编译出的代码

var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var MyError = /** @class */ (function (_super) {
__extends(MyError, _super);
function MyError() {
// 问题关键在这里
return _super !== null && _super.apply(this, arguments) || this;
}
return MyError;
}(Error));

原因在于,Error 是一个特殊的存在,即是一个构造函数,也是一个普通函数。以下两种调用皆可返回 error object。

Error('message')
new Error('message')

那么在调用以下函数时,_super 为 Error,返回的即是 Error(this, arguments),而不是 this

_super !== null && _super.apply(this, arguments) || this;

在 Typescript 中

翻了翻文档,Typescript 2.1 的一些 breaking change 导致对于一些原生对象 (Error/Array/Map) 的继承无法正常工作,应该就是由 generated code 的改变造成的。官方给出的一个建议是:

class FooError extends Error {
constructor(m: string) {
super(m)
// 在 super 之后立刻调用,改变实例的 prototype.
Object.setPrototypeOf(this, FooError.prototype)
}
}

但是这个写法其实相当的傻,因为对于每一个子类的构建函数来说,在改变原型之前,是无法拿到正确的子类实例'this.constructor' 的,所以 Object.setPrototypeOf 需要出现在所有子类的构建函数中。

解决方案

只好把原型继承拿回来了,最终在 target 为 es5 及以下的解决方案:

export class ExtensibleError implements Error {
message: string
name: string

constructor(message?: string) {
Error.apply(this, arguments)

this.message = message || ''
this.name = this.constructor.name

if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
}
}
}

ExtensibleError.prototype = Object.create(Error.prototype)

构建一个中间的辅助类,并不直接采用 class 继承 Error,而只实现 Error 接口,采用原型继承,此类的示例可经过 instanceOf 的检验。通过 Error.captureStackTrace 在初始化此类实例时能够捕获调用栈。

class MyError extends ExtensibleError {
}

const err = new MyError()
err instanceof ExtensibleError // true
err instanceof MyError // true
err instanceof Error // true

如果编译目标为 ES6 以上呢?

此时编译器就不需要去帮你转化 class 的实现了,会把你的代码原样输出:

** 但是!** 之前的解决方案在 nodejs 中运行会报错:

ExtensibleError.prototype = Object.create(Error.prototype);
^
TypeError: Cannot assign to read only property 'prototype' of function 'class ExtensibleError...

因为使用 class 关键字声明的 ExtensibleError 是一个叫做类构造器(class constructor)的特殊函数,它的 prototype 是只读的,试图去改变它的话,只有报错(nodejs)和不生效两种可能。

以下的代码在现在的 chrome(V8) 和 firefox(SpiderMonkey) 引擎中执行结果都是一样。

class ExtensibleError {}
ExtensibleError.prototype = Object.create(Error.prototype);
const e = new ExtensibleError;
e instanceof Error // false

如果 target 是 ES6 以上的,简简单单写 class ExtensibleError extends Error {} 就行了。

同时我们的解决方案为了在不同编译 target 下都能正常工作,可以加入一个运行时的检测。

/** 检测当前 runtime 是否支持 es6 class */
let isClassSupported = false
try {
isClassSupported = ((class Test {}).toString().indexOf('class') > -1)
} catch (error) {
}

if (!isClassSupported) {
ExtensibleError.prototype = Object.create(Error.prototype)
}

参考