この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部 IoT事業部の若槻です。
今回は、TypeScriptでのCommonJSなモジュールの実行のエラーを、esModuleinterop
フラグの設定により回避できたので、書き残しておきます。
事象
snakecase-keysを使ってオブジェクトのキーをスネークケースに変換するスクリプトを作成しました。
script.ts
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
を実行してトランスパイルします。
script.js
"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
に設定したら解決しました。
tsconfig.json
{
"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とはなっていませんね。
node_modules/snakecase-keys/index.js
'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として扱うようになっていることが分かります。
script.js
"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);
以上