npmでインストールしたmoduleのクラスをextendしたら TypeError: Class constructor classname cannot be invoked without 'new'

表題のエラーに遭遇したので、その原因と対処法を記録しておきます。

環境

この記事は以下の環境を前提にしています。

▼package.json

"devDependencies"{
  "@babel/cli": "^7.10.5",
  "@babel/core": "^7.10.5",
  "@babel/preset-env": "^7.10.4",
  "typescript": "^3.8.3"
}

記事を読む前に、お手元の環境をご確認ください。

問題発生の手順

  1. npmでES6ネイティブのモジュールをインストールする。
  2. モジュール内のクラスを継承する。
  3. BabelなどでES5にトランスパイルする。
import { ES6Class } from "module";
export class ClassName extends ES6Class {...

上記コードの例の場合、npmからES6ネイティブのmoduleという名前のパッケージをインストールし、 ES6Classというクラスをインポート、ClassNameというクラスに継承しています。

症状

上記の手順を実行し、生成されたjavascriptを実行すると、以下のエラーが発生します。

Uncaught TypeError: Class constructor ClassName cannot be invoked without 'new'
    at new ClassName (ClassName.js:35)
    at onDomContentsLoaded (main.js:19)
    at eval (main.js:32)
    at Module../src/main.js (bundle.js:133)
    at __webpack_require__ (bundle.js:20)
    at bundle.js:84
    at bundle.js:87
classname	@	classname.js:35
onDomContentsLoaded	@	main.js:19
(anonymous)	@	main.js:32
./src/main.js	@	bundle.js:133
__webpack_require__	@	bundle.js:20
(anonymous)	@	bundle.js:84
(anonymous)	@	bundle.js:87

ClassNameクラスのコンストラクターの実行に失敗します。

原因

この問題は、ネイティブのES6クラスを継承し、さらにES5に変換することによって発生します。この変換に失敗するため、上記の症状が発生します。

対処法

あなたが問題の発生するパッケージの利用者か、開発者かによって対処法が分かれます。

あなたがパッケージの利用者の場合

変換の対象をES5ではなくES6に指定することで、この問題は解決します。

Babelの場合

Babelでトランスパイルを使用している場合、@babel/preset-envで変換対象を指定します。 nodeを変換対象に指定することでES6のjsファイルが出力されます。

▼.babelrc

{
  "presets": [
    [ "@babel/preset-env", {
      "targets": { "node": true }
    }]
  ]
}

TypeScriptの場合

TypeScriptの場合、コンパイラオブションで変換対象の指定ができます。

▼tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
  }
}

副作用

変換先のjsファイルはES6の構文を使用していますので、IE11では動作しません。

ECMAScript 6 compatibility table

IE11をターゲットとする場合は、extendsを使用しないなど他の回避方法を検討してください。

あなたがパッケージの開発者の場合

問題が発生するパッケージを、CommonJSESModulesの両対応にすることで問題が解決します。npmモジュールに両方のスクリプトがあり、その状態がpackage.jsonに記述されていれば、webpackなどのモジュールバンドラーがファイルを適切に取りあつかってくれます。

TypeScriptの場合

まずはCommonJSを出力するtsconfig.jsonを用意します。この設定では、仮にlibというディレクトリにCommonJSと型定義ファイルが出力されます。

▼tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": true,
    "declarationMap": true,
    "outDir": "./lib"
  }
}

tsconfig.json継承するtsconfig.esm.jsonを用意します。esmというディレクトリに、ESModulesが出力されます。こちらでは型定義ファイルは出力しません。

▼tsconfig.esm.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "target": "es6",
    "module": "es2015",
    "outDir": "./esm",
    "declaration": false,
    "declarationMap": false
  }
}

最後に、package.jsonにこの2つのファイルの場所を記述します。

▼package.json

{
  "main": "./lib/index.js",
  "types": "./lib/index.d.ts",
  "module": "./esm/index.js",
}

package.jsonにmoduleフィールドを追加し、ESModules形式で出力されたファイルを指定します。モジュールバンドラーはこのフィールドを参照して、必要なファイルを取り出します。

参考記事

stackoverflow - Javascript ES6 TypeError: Class constructor Client cannot be invoked without 'new'