個人的にちょうど該当の問題を取り扱ったので、TypeScript 2.7.1 で導入された
esModuleInterop オプションの役割と背景についてまとめておく。
なお、本来は export と import の両方に影響するオプションだが、この記事では外部の npm パッケージを import する状況のみを取り上げる。
TL;DR
Node.js 環境で import/export を使う場合、
- esModuleInterop オプションを積極的に有効にする。
- 関数や class を export するモジュールを import する場合、
import * as _ from '_'
のかわりにimport _ = require('_')
を使う。
モジュールの互換性
先にTypeScript の ES6 modules の解釈と allowSyntheticDefaultImports の整理という記事を読んでおくとよい。
バックグラウンドとして知っておくべきは以下の2つ:
- ES6 Modules は Node.js のモジュール(CommonJS)との互換性について規定がない。
- ES6 Modules で import/export できるものは TypeScript でいう名前空間(ざっくりいうと識別子として使えるプロパティを持つプレーンなオブジェクト) にかぎられるが、CommonJS はそうした制限がない。
Babel V.S. TypeScript
上の事情のため、Node.js のモジュールをどう扱うかはビルドツールごとに独自の仕様が設けられている。
例えば、つぎのような math モジュールを取り込もうとすると、TypeScript と Babel で異なる書き方が必要だった。
var math = {
min: function (lhs, rhs) { ...}
};
module.exports = math;
Babel:
import math from 'math';
math.min(1, 2);
TypeScript:
import * as math from 'math';
math.min(1, 2);
いいかえると、 export 側のコードはそれぞれ以下の ES6 Module として解釈されている。
Babel:
function min(lhs, rhs) { ...}
export default {
min,
};
TypeScript:
export function min(lhs, rhs) { ...}
esModuleInterop オプションはこの振る舞いを Babel に近づけるものと考えてよい。
関数やクラスの export 問題
では TypeScript 式の何が問題だったのかというと、すでにある CommonJS モジュールが class や function を export している場合に手詰まりになること。
例えば
function min(lhs, rhs) { ... }
module.exports = min;
というモジュールがあったとして、以下のコードを babel に通して実行すると、実行時エラーになる。
import * as min from 'min';
min(2, 3);
どう解決されるのか
これに対して上のオプションをつけるとどうなるのか。
まず import *
と 関数呼び出しの組み合わせがコンパイルエラーになるようになる。
正しく動かしたい場合、選択肢は二つある。
一つは、以前からある syntheticDefault オプションを使用すること。ただしこのオプションはあらゆるモジュールの import/export の振る舞いを変えてしまうため、おすすめしない。
もう一つは import =
を使うこと。つまり以下のようにする。
import min = require('min');
もともと互換性がない以上、これが妥当だろう。実際、DefinitelyTyped はそうしている。