Typescript 中继承 Error 对象的坑

事情起因是在写 Nodejs 服务时,想要统一捕获、处理业务异常错误,于是用到了自定义 Http Error,这个自定义的错误是通过继承 Error 对象来实现的。

但是在中间件中进行错误捕获时,instanceof 却无法得到预期的结果,百思不得其解。仔细想了下,因为使用了 ES6 的 extends 语法,唯一的可能性就是 typescript 编译时,出现了问题。

1. 问题复现

有如下代码:

class MyError extends Error {}

const err = new MyError();

console.log(err instanceof MyError);

使用 tsc 编译为 es5 后执行,预期得到结果应该是 true,但实际上,你只会得到 false。

# npx tsc extends.ts
# node extends.js   
false

这是为什么呢?我们接着分析。

2. 问题分析

首先我们打开编译后的 js 文件看下。

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        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));
var err = new MyError();
console.log(err instanceof MyError);

可以看到,ts 编译的 target 肯定不是 es6,所有会多了这么多的帮助函数。不过我们不关心这些,我们关心的只是为什么 instanceof 失效了。

这里就回到了 js 的基础知识,调用 new 关键字后,返回的对象应该是构造函数的实例或者返回的对象,如果构造函数里的返回值是 undefined,那么返回构造函数的实例,所以 instanceof 失效的话,有问题的地方只可能是这段代码:

function MyError() {
    return _super !== null && _super.apply(this, arguments) || this;
}

这里的 _super 指的是 Error 构造函数,而这里的逻辑优先返回的是 Error 构造函数返回的结果,而不是 this。

一般来说,直接调用构造函数的返回值都是 undefined,但是不幸的是,Error 返回的却是 error 实例,也就导致了 MyError 的实例其实是 Error 的实例。这点可以通过代码验证。

class MyError extends Error {}

const err = new MyError();

console.log(err instanceof Error);

// true

关于 typescript 为什么这么做,官方也有对应的解释:breaking changes

3. 解决方法

typescript 官方其实也给出了相应的方案,就是重新设置 this 的原型对象。

class MyError extends Error {
  constructor() {
    super()
    Object.setPrototypeOf(this, MyError.prototype);
  }
}

const err = new MyError();

console.log(err instanceof MyError); // true

4. 另一种方案

在网上搜索时,也发现了网友的另一种方案。因为前面说了,直接调用 Error 构造函数也会返回一个 error 实例,那我们可不可以改变这种行为?答案当然是可以的,本质上其实就是实现一个 Error 对象。因为个人觉得这样做跟 ts 帮你实现并没有什么本质上区别,这里就贴一下原理代码,不做过多深入。

class MyError implements Error {
  message: string
  name: string

  constructor(...args: any[]) {
    Error.apply(this, args)
  }
}
MyError.prototype = Error.prototype

const err = new MyError();

console.log(err instanceof MyError); // true
console.log(err instanceof Error); // true