TypeScriptのesModuleinteropフラグを設定してCommonJSモジュールを実行可能とする
こんにちは、CX事業本部 IoT事業部の若槻です。
今回は、TypeScriptでのCommonJSなモジュールの実行のエラーを、esModuleinterop
フラグの設定により回避できたので、書き残しておきます。
事象
snakecase-keysを使ってオブジェクトのキーをスネークケースに変換するスクリプトを作成しました。
import snakecaseKeys from 'snakecase-keys'; const camel = { aaaBbb: 'ccc', dddEdd: 'fff' }; const snake = snakecaseKeys(camel); console.log(snake);
しかしこのスクリプトを実行するとエラーとなってしまいます。
$ npx ts-node script.ts Need to install the following packages: ts-node Ok to proceed? (y) y /Users/wakatsuki.ryuta/projects/0321test/script.ts:6 const snake = snakecaseKeys(camel); ^ TypeError: snakecase_keys_1.default is not a function at Object.<anonymous> (/Users/wakatsuki.ryuta/projects/0321test/script.ts:6:28) at Module._compile (internal/modules/cjs/loader.js:1068:30) at Module.m._compile (/Users/wakatsuki.ryuta/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1056:23) at Module._extensions..js (internal/modules/cjs/loader.js:1097:10) at Object.require.extensions.<computed> [as .ts] (/Users/wakatsuki.ryuta/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/index.ts:1059:12) at Module.load (internal/modules/cjs/loader.js:933:32) at Function.Module._load (internal/modules/cjs/loader.js:774:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) at main (/Users/wakatsuki.ryuta/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/bin.ts:198:14) at Object.<anonymous> (/Users/wakatsuki.ryuta/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/src/bin.ts:288:3)
調査
エラーメッセージによるとconst snake = snakecaseKeys(camel);
でTypeError: snakecase_keys_1.default is not a function
となっているようです。
npx tsc script.ts
を実行してトランスパイルします。
"use strict"; exports.__esModule = true; var snakecase_keys_1 = require("snakecase-keys"); var camel = { aaaBbb: 'ccc', dddEdd: 'fff' }; var snake = (0, snakecase_keys_1["default"])(camel); console.log(snake);
上記コンパイル結果を実行するとやはりエラーとなります。
$ node script.js /Users/wakatsuki.ryuta/projects/0321test/script.js:5 var snake = (0, snakecase_keys_1["default"])(camel); TypeError: (0 , snakecase_keys_1.default) is not a function at Object.<anonymous> (/Users/wakatsuki.ryuta/projects/0321test/script.js:5:45) at Module._compile (internal/modules/cjs/loader.js:1068:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10) at Module.load (internal/modules/cjs/loader.js:933:32) at Function.Module._load (internal/modules/cjs/loader.js:774:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) at internal/main/run_main_module.js:17:47
解決
tsconfig.json
でcompilerOptions
としてesModuleInterop
フラグをtrue
に設定したら解決しました。
{ "compilerOptions": { "esModuleInterop": true, } }
スクリプトが正常に実行できるようになっています。
$ npx ts-node script.ts Need to install the following packages: ts-node Ok to proceed? (y) y { aaa_bbb: 'ccc', ddd_edd: 'fff' }
どういうことなのか?
ドキュメントにesModuleInterop
についての解説がありました。
By default (with esModuleInterop false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. In doing this, there are two parts in particular which turned out to be flawed assumptions:
- a namespace import like import * as moment from "moment" acts the same as const moment = require("moment")
- a default import like import moment from "moment" acts the same as const moment = require("moment").default
TypeScriptのデフォルトではCommonJSなどのモジュールをES6と同様に扱うため、import * as moment from "moment"
のようなnamespace importではdefault importはconst moment = require("moment")
、import moment from "moment"
のようなdefault importはconst moment = require("moment").default
と扱われるとのことです。
今回のsnakecase-keys
の場合は、モジュール側でdefault exportされていないにも関わらず、スクリプト側でdefault importして使用しようとしたため起こったエラーのようです。
snakecase-keys
のソースを見ると確かにdefault exportとはなっていませんね。
'use strict' const map = require('map-obj') const { snakeCase } = require('snake-case') module.exports = function (obj, options) { options = Object.assign({ deep: true, exclude: [] }, options) return map(obj, function (key, val) { return [ matches(options.exclude, key) ? key : snakeCase(key), val ] }, options) } function matches (patterns, value) { return patterns.some(function (pattern) { return typeof pattern === 'string' ? pattern === value : pattern.test(value) }) }
そしてnpx tsc script.ts --esModuleInterop true
と実行した場合のトランスパイル結果を見ると、モジュールがdefault exportされていない場合はdefaultとして扱うようになっていることが分かります。
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; exports.__esModule = true; var snakecase_keys_1 = __importDefault(require("snakecase-keys")); var camel = { aaaBbb: 'ccc', dddEdd: 'fff' }; var snake = (0, snakecase_keys_1["default"])(camel); console.log(snake);
以上