30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

TypeScript の esModuleInterop フラグについて

esModuleInteropフラグを有効にすると、コンパイル時にヘルパーメソッドが生成されるようになり、モジュールシステムの相互運用性が高まる。
これにより、defaultをエクスポートしていない CommonJS 形式のモジュールを、ES Modules でデフォルトインポートする、といったことが可能になる。

この記事の内容は、TypeScript のv3.9.6と Node.js のv12.17.0で動作確認している。

ES Modules と CommonJS の互換性

まず、検証用に以下のファイルを作成する。

// src/myModule.js
const add = (a, b) => a + b;

module.exports = {
  value: 123,
  add,
};
// src/index.ts
import {value, add} from './myModule';

console.log(value);
console.log(add(3, 4));

src/myModule.jsは CommonJS 形式でエクスポートしており、それをsrc/index.tsでインポートして使っている。

そして、tsconfig.jsonを以下の内容にした上で、コンパイルする。

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "CommonJS",
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": false,
    "moduleResolution": "Node",
    "allowJs": true
  },
  "include": ["./src/**/*"]
}

そうするとdist/index.jsが生成されるので、実行してみる。

$ node dist/index.js
123
7

無事に実行される。

インポートに*を使っても、問題ない。

import * as myModule from './myModule';

console.log(myModule.value);
console.log(myModule.add(3, 4));

問題は、デフォルトインポートしたときである。

// can only be default-imported using the 'esModuleInterop' flagts(1259)
import myModule from './myModule';

console.log(myModule.value);
console.log(myModule.add(3, 4));

コンパイルエラーになってしまう。
以下のようにdefaultをエクスポートしていればそれを読み込めるが、CommonJS ではこの書き方はあまり一般的ではない。

module.exports = {
  default: 1,
};

例えば、@types/nodeパッケージをインストールすることで、 Node.js の標準モジュールを使ったコードを書けるようになるが、以下のコードはエラーになる。

// Module '"fs"' has no default export.ts(1192)
import fs from 'fs';

console.log(fs.readFile);

サードパーティライブラリでも同様の問題が起こり得る。
例えば React で発生する。

// can only be default-imported using the 'esModuleInterop' flagts(1259)
import React from 'react';

console.log(React.memo);

allowSyntheticDefaultImports

allowSyntheticDefaultImportsフラグを有効にすることで、コンパイルエラーを回避できるようになる。

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "CommonJS",
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": false,
    "moduleResolution": "Node",
    "allowJs": true,
    "allowSyntheticDefaultImports": true // これを追加
  },
  "include": ["./src/**/*"]
}

これで、以下のコードもコンパイルできるようになった。

import fs from 'fs';

console.log(fs.readFile);

だが残念ながら、コンパイルしたコードを実行するとエラーになる。

TypeError: Cannot read property 'readFile' of undefined

allowSyntheticDefaultImportsは型チェックにのみ影響を与えるフラグで、生成される JavaScript には影響しない。
そのため、相変わらずimport fs from 'fs';fsモジュールを読み込むことはできず、fsundefinedになってしまう。

esModuleInterop

実際にデフォルトインポートするためには、esModuleInteropフラグを有効にする。
そうすることで CommonJS と相互運用可能なコードが出力されるようになり、デフォルトインポートも使えるようになる。
また、このフラグを有効にすると自動的にallowSyntheticDefaultImportsも有効になる。

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "CommonJS",
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": false,
    "moduleResolution": "Node",
    "allowJs": true,
    "esModuleInterop": true // これを追加
  },
  "include": ["./src/**/*"]
}

これで、デフォルトインポートできるようになった。

import fs from 'fs';

console.log(fs.readFile); // [Function: readFile]

生成されたdist/index.jsを見てみると、__importDefaultというヘルパーメソッドが生成されており、これによってデフォルトインポートが可能になっている。

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
console.log(fs_1.default.readFile);

また、*を使ったインポート文を書くと、__importStarが生成される。

import * as fs from 'fs';

console.log(fs.readFile);
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
console.log(fs.readFile);

参考資料