TypeScriptでexport defaultにする際の注意事項と解決方法
はじめに
こんにちは、SHIFTの開発部門に所属している Katayama です。
TypeScript の型定義において、その型(type hoge = {...})を"export default"(デフォルトエクスポート)している場合に、Node.js にコンパイルして実行した際に、以下のようなエラーが出てしまった。
$ node build/app.js
/home/study/workspace/ts-node-oidc/build/app.js:4
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
^
TypeError: (0 , kebabcase_keys_1.default) is not a function
at Object.<anonymous> (/home/study/workspace/ts-node-oidc/build/app.js:4:45)
at Module._compile (node:internal/modules/cjs/loader:1196:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1250:10)
at Module.load (node:internal/modules/cjs/loader:1074:32)
at Function.Module._load (node:internal/modules/cjs/loader:909:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:22:47
今回は、型定義で"export default"を利用する際の注意についてみていきたいと思う。またおまけとして、型定義が"export = ..."で export されている場合の TypeScript 内でエラーなく利用するための実装についても見ていきたいと思う。
エラーの解決方法
エラーの発生状況について
まず、どのようなプロジェクトの設定・TypeScript の実装をしていた場合に、エラーになったか?を整理する。
package.json の"type"は"commnjs"。
{
...
"type": "commonjs",
...
}
tsconfig.json は以下("esModuleInterop"は false)。
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"outDir": "./build",
"declaration": true,
"sourceMap": true,
"esModuleInterop": false
},
"include": ["srv/**/*.ts"],
"exclude": ["node_modules", "build"]
}
TypeScript の実装は以下。
import kebabcaseKeys from 'kebabcase-keys';
const result = kebabcaseKeys({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);
"kebabcase-keys"の型定義は以下のように"export default"であるとする(@types/kebabcase-keysは"export = ..."になっているが、今回はローカル環境で独自に型定義を行い、"export default function..."になっているとする)。
export default function kebabcaseKeys<T extends ..., OptionsType extends ...>(
input: T,
options?: OptionsType,
): KebabCasedProperties<T, ..., ...>;
エラーの詳細について
上記のような状況において、"tsc"コマンドで TypeScript をコンパイルした後、Node.js を実行すると「はじめに」に取り上げていたエラーが発生する。このエラーが発生している Node.js のコードは以下。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = require("kebabcase-keys");
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);
//# sourceMappingURL=app.js.map
エラーメッセージからエラーの発生個所は、"(0, kebabcase_keys_1.default)"である事が分かる。
エラーの原因と解決方法
エラーの原因は、TypeScript が CommonJS/AMD/UMD モジュールを ES6 モジュールと同様に扱う事であり、その詳細についてはES Module Interop - esModuleInteropに記載がある。つまりは、モジュールに関する仕様の部分でミスマッチが発生しエラーになっている。
そこでそのミスマッチを埋めてくれるオプションとしてesModuleInteropがあり、これを true に設定すると、このモジュールの仕様におけるミスマッチをうまく解消してくれるようになる。
エラー発生時の tsconfig.json の"esModuleInterop"を true に変更して、TypeScript をコンパイル後、Node.js を実行してみると、エラーなく実行できる事が確認できる。
具体的に"esModuleInterop"により何が変わるか?だが、コンパイル後の Node.js のコードを見ると、以下のように"__importDefault"というヘルパー関数が追加されており、これによりデフォルトインポートを解決できるようになった。
// "esModuleInterop": trueの場合
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = __importDefault(require("kebabcase-keys"));
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);
//# sourceMappingURL=app.js.map
上記の事から、"export default"を利用して CommonJS にコンパイルする場合には、"esModuleInterop"オプションが必要である事が分かる。そしてこの注意点については、ちゃんとDefault Exportsにも書かれている。
まとめ
今回は TypeScript の型定義において、"export default"(デフォルトエクスポート)を利用している場合の注意点として、"esModuleInterop"オプションが必要である事をみてきた。公式に書かれている注意事項の意味を実際にエラーを通じて理解を深められたのは良かったのではないかと思っている。
※おまけでは、どこでも利用できる"export = "で型定義を行った時に、TypeScript で利用する方法についてみていきたいと思う。
おまけ
型定義の"export ="について
Default Exportsに書かれていたような方法(型定義を"export = "で行う)場合に、TypeScript で利用する際にはどうするか?について少なくとも 2 つの方法があるので、それぞれ見ていく。
import module = require("module") を利用する
"esModuleInterop": true を利用する
1. import module = require("module") を利用する
これはexport = and import = require()に書かれている方法で、CommonJS 形式のモジュールを扱うための TypeScript 固有の記法を利用するもの。
実装としては以下のようになる(かなり違和感はあるが…)。
import kebabcaseKeys = require("kebabcase-keys");
const result = kebabcaseKeys(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
コンパイルされた後のコードは以下。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcaseKeys = require("kebabcase-keys");
const result = kebabcaseKeys(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map
上記は、package.json の"type": "commonjs"で、tsconfig.json は以下の場合。
"esModuleInterop": false
かつ
"module": "Node16",
"moduleResolution": "node16"
"moduleResolution": "node"だと、以下のようにコンパイルエラーになる("type": "module"で、"moduleResolution": "node"の場合も同様)。
また、package.json の"type": "module"で、tsconfig.json が以下の場合は、コンパイル後の Node.js は次のようになる。
"esModuleInterop": false
かつ
"module": "Node16",
"moduleResolution": "node16"
import { createRequire as _createRequire } from "module";
const __require = _createRequire(import.meta.url);
const kebabcaseKeys = __require("kebabcase-keys");
const result = kebabcaseKeys(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map
2. "esModuleInterop": true を利用する
これは上記の"export default"を利用する場合で取り上げた"esModuleInterop"オプションを true にする事で、モジュール間の互換を取れるようにする方法。
実装としては以下のようになる。
import kebabcaseKeys from "kebabcase-keys";
const result = kebabcaseKeys(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
コンパイルされた後のコードは以下。
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = __importDefault(require("kebabcase-keys"));
const result = (0, kebabcase_keys_1.default)(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map
上記は、package.json の"type": "commonjs"で、tsconfig.json が以下の場合。
"esModuleInterop": true
かつ
"module": "Node16",
"moduleResolution": "node"
上記の設定を "moduleResolution": "node16" にしても、この場合は同じコンパイル結果になる。
また、package.json の"type": "module"で、tsconfig.json が以下の場合、コンパイル後の Node.js は次のようになる。
"esModuleInterop": true
かつ
"module": "Node16",
"moduleResolution": "node16"
import kebabcaseKeys from "kebabcase-keys";
const result = kebabcaseKeys(
{ foo_bar: "baz", nested: { foo_Baz: new Date() } },
{ deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map
ただし、"moduleResolution": "node"の場合は、CommomJS にコンパイルされてしまうので以下のようにコンパイルはできても実行時にエラーになる(package.json で ESM のプロジェクト設定になっているので)。
《この公式ブロガーの記事一覧》
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/