通过 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 需要出现在所有子类的构建函数中。

解决方案

只好把原型继承拿回来了,最终解决方案:

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 {} 就行了。

参考