TypeScriptのesModuleinteropフラグを設定してCommonJSモジュールを実行可能とする

2022.03.21

こんにちは、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.jsoncompilerOptionsとして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);

以上