モジュール - ESM/CJS 相互運用性

時は2015年、あなたはESMからCJSへのトランスパイラを書いています。これを行う方法に関する仕様はありません。あなたが持っているのは、ESモジュールが互いにどのように相互作用するかについての仕様、CommonJSモジュールが互いにどのように相互作用するかについての知識、そして物事を理解するためのコツだけです。エクスポートするESモジュールを考えてみましょう。

ts
export const A = {};
export const B = {};
export default "Hello, world!";

これをどのようにしてCommonJSモジュールに変換しますか? デフォルトエクスポートは特別な構文を持つ名前付きエクスポートであることを思い出せば、選択肢は1つしかないようです。

ts
exports.A = {};
exports.B = {};
exports.default = "Hello, world!";

これは素晴らしいアナロジーであり、インポート側にも同様のものを実装できます。

ts
import hello, { A, B } from "./module";
console.log(hello, A, B);
// transpiles to:
const module_1 = require("./module");
console.log(module_1.default, module_1.A, module_1.B);

これまでのところ、CJSの世界のすべてはESMの世界のすべてと1対1で対応しています。上記の等価性をさらに一歩拡張すると、次のようにもなります。

ts
import * as mod from "./module";
console.log(mod.default, mod.A, mod.B);
// transpiles to:
const mod = require("./module");
console.log(mod.default, mod.A, mod.B);

このスキームでは、`exports`に関数、クラス、またはプリミティブが割り当てられる出力を生成するESMエクスポートを記述する方法がないことに気付くかもしれません。

ts
// @Filename: exports-function.js
module.exports = function hello() {
console.log("Hello, world!");
};

しかし、既存のCommonJSモジュールは頻繁にこの形式を取ります。 トランスパイラで処理されたESMインポートは、このモジュールにどのようにアクセスするのでしょうか? 名前空間インポート(`import *`)はプレーンな`require`呼び出しに変換されることを確立したので、次のような入力をサポートできます。

ts
import * as hello from "./exports-function";
hello();
// transpiles to:
const hello = require("./exports-function");
hello();

出力は実行時に機能しますが、コンプライアンスの問題があります。JavaScriptの仕様によると、名前空間インポートは常に *モジュール名前空間オブジェクト*、つまり、メンバーがモジュールのエクスポートであるオブジェクトに解決されます。 この場合、`require`は関数`hello`を返しますが、`import *`は関数を返すことはできません。想定した対応は無効のようです。

ここで一歩下がって、*目標*は何であるかを明確にする価値があります。モジュールがES2015仕様に登場するとすぐに、トランスパイラはESMをCJSにダウンレベル化するサポートを提供し、ユーザーはランタイムがサポートを実装するずっと前に新しい構文を採用できるようになりました。ESMコードを書くことは、新しいプロジェクトを「将来にわたって使える」ようにするための良い方法であるという感覚さえありました。これが真実であるためには、トランスパイラのCJS出力の実行から、ランタイムがネイティブにサポートするようになったら真のESM入力の実行へのシームレスな移行パスが必要でした。目標は、ダウンレベルのESMをCJSに変換する方法を見つけることであり、これにより、動作に目に見える変化がない状態で、将来のランタイムで、変換された出力のいずれかまたはすべてを真のESM入力に置き換えることができました。

仕様に従うことにより、トランスパイラは、変換されたCommonJS出力のセマンティクスがESM入力の指定されたセマンティクスと一致する一連の変換を簡単に見つけることができました(矢印はインポートを表します)。

A flowchart with two similar flows side-by-side. Left: ESM. Right: ESM transpiled to CJS. In the ESM flow: "Importing module" flows to "Imported module" through arrow labeled "specified behavior". In the ESM transpiled to CJS flow: "Importing module" flows to "Imported module" through arrow labeled "designed based on spec".

しかし、CommonJSモジュール(CJSに変換されたESMではなく、CommonJSとして記述されたもの)はNode.jsエコシステムで既に確立されていたため、ESMとして記述され、CJSに変換されたモジュールがCommonJSとして記述されたモジュールを「インポート」し始めることは避けられませんでした。ただし、この相互運用性の動作はES2015では指定されておらず、実際の実行時にはまだ存在していませんでした。

A flowchart with three areas side-by-side. Left: ESM. Middle: True CJS. Right: ESM transpiled to CJS. Left: ESM "Importing module" flows to ESM "Imported module" through arrow labeled "specified behavior," and to True CJS "Imported module" through dotted arrow labeled "unspecified behavior." Right: ESM transpiled to CJS "Importing module" flows to ESM transpiled to CJS "Imported module" through arrow labeled "designed based on spec," and to True CJS "Imported module" through dotted arrow labeled "❓🤷‍♂️❓"

トランスパイラの作成者が何もしなくても、変換されたコードで出力された`require`呼び出しと既存のCJSモジュールで定義された`exports`の間の既存のセマンティクスから動作が出現します。そして、ユーザーがランタイムでサポートされるようになったら、変換されたESMから真のESMにシームレスに移行できるようにするために、その動作はランタイムが実装することを選択した動作と一致する必要があります。

ランタイムがどの相互運用動作をサポートするかを推測することは、ESMが「真のCJS」モジュールをインポートする場合にも限定されませんでした。ESMがCJSから変換されたESMをCJSと区別できるかどうか、CJSがESモジュールを`require`できるかどうかについても、指定されていませんでした。ESMインポートがCJS`require`呼び出しと同じモジュール解決アルゴリズムを使用するかどうかさえも不明でした。トランスパイラユーザーにネイティブESMへのシームレスな移行パスを提供するには、これらすべての変数を正しく予測する必要がありました。

`allowSyntheticDefaultImports` と `esModuleInterop`

`import *` が `require` に変換されるという、仕様コンプライアンスの問題に戻りましょう。

ts
// Invalid according to the spec:
import * as hello from "./exports-function";
hello();
// but the transpilation works:
const hello = require("./exports-function");
hello();

TypeScriptが初めてESモジュールの記述とトランスパイルのサポートを追加したとき、コンパイラは、exportsが名前空間のようなオブジェクトではないモジュールの名前空間インポートでエラーを発行することで、この問題に対処しました。

ts
import * as hello from "./exports-function";
// TS2497 ^^^^^^^^^^^^^^^^^^^^
// External module '"./exports-function"' resolves to a non-module entity
// and cannot be imported using this construct.

唯一の回避策は、ユーザーがCommonJSのrequireを表す古いTypeScriptインポート構文に戻る事でした。

ts
import hello = require("./exports-function");

ユーザーに非ESM構文への復帰を強制することは、本質的に「将来、"./exports-function"のようなCJSモジュールがESMインポートでアクセス可能になるかどうか、またはどのようにアクセス可能になるかはわかりませんが、使用しているトランスパイルスキームでは、実行時に機能するとしても、import *ではアクセスできないことはわかっています」ということを認めることでした。このファイルをそのまま実際のESMに移行できるようにするという目標は達成されていませんが、import *を関数にリンクさせるという代替案でも達成されていません。これは、allowSyntheticDefaultImportsesModuleInteropが無効になっている場合のTypeScriptの今日の動作です。

残念ながら、これは少し単純化しすぎです。TypeScriptはこのエラーでコンプライアンスの問題を完全に回避したわけではありません。なぜなら、関数宣言が名前空間宣言とマージされている限り、関数の名前空間インポートが機能し、呼び出しシグネチャを保持することを許可していたからです。名前空間が空の場合でもです。そのため、ベア関数をエクスポートするモジュールは「非モジュールエンティティ」として認識されていましたが、

ts
declare function $(selector: string): any;
export = $; // Cannot `import *` this 👍

意味のないはずの変更により、無効なインポートがエラーなしで型チェックできるようになりました。

ts
declare namespace $ {}
declare function $(selector: string): any;
export = $; // Allowed to `import *` this and call it 😱

一方、他のトランスパイラは同じ問題を解決する方法を考え出していました。思考プロセスは次のようになりました。

  1. 関数またはプリミティブをエクスポートするCJSモジュールをインポートするには、明らかにデフォルトインポートを使用する必要があります。名前空間インポートは不正であり、名前付きインポートはここでは意味がありません。
  2. おそらく、これは、ESM / CJS相互運用を実装するランタイムが、CJSモジュールのデフォルトインポートを、exportsが関数またはプリミティブの場合にのみ行うのではなく、*常に* exports全体に直接リンクすることを選択することを意味します。
  3. したがって、真のCJSモジュールのデフォルトインポートは、require呼び出しと同様に機能するはずです。ただし、トランスパイルされたCJSモジュールと真のCJSモジュールを区別する方法が必要になります。そのため、export default "hello"exports.default = "hello"にトランスパイルし、*その*モジュールのデフォルトインポートをexports.defaultにリンクさせることができます。基本的に、トランスパイルされた独自のモジュールの1つのデフォルトインポートは、ある方法で機能する必要があり(ESMからESMへのインポートをシミュレートするため)、他の既存のCJSモジュールのデフォルトインポートは、別の方法で機能する必要があります(ESMからCJSへのインポートがどのように機能するかをシミュレートするため)。
  4. ESモジュールをCJSにトランスパイルするときは、出力に特別な追加フィールドを追加しましょう
    ts
    exports.A = {};
    exports.B = {};
    exports.default = "Hello, world!";
    // Extra special flag!
    exports.__esModule = true;
    デフォルトインポートをトランスパイルするときにチェックできます
    ts
    // import hello from "./modue";
    const _mod = require("./module");
    const hello = _mod.__esModule ? _mod.default : _mod;

__esModuleフラグは、最初にTraceurに、次にBabel、SystemJS、Webpackに間もなく登場しました。TypeScriptは1.8でallowSyntheticDefaultImportsを追加し、型チェッカーがデフォルトインポートをexports.defaultではなく、export default宣言のないモジュールタイプのexportsに直接リンクできるようにしました。このフラグは、インポートまたはエクスポートの出力方法を変更しませんでしたが、デフォルトインポートが他のトランスパイラの処理方法を反映することを許可しました。つまり、デフォルトインポートを使用して「非モジュールエンティティ」に解決することを許可しました。ここで、import *はエラーでした

ts
// Error:
import * as hello from "./exports-function";
// Old workaround:
import hello = require("./exports-function");
// New way, with `allowSyntheticDefaultImports`:
import hello from "./exports-function";

これは通常、BabelとWebpackのユーザーがTypeScriptの文句なしにそれらのシステムですでに機能しているコードを記述するのに十分でしたが、部分的な解決策に過ぎず、いくつかの問題は未解決のままでした。

  1. Babelなどは、ターゲットモジュールに__esModuleプロパティが見つかったかどうかによってデフォルトのインポート動作を変更しましたが、allowSyntheticDefaultImportsは、ターゲットモジュールのタイプにデフォルトのエクスポートが見つからなかった場合にのみ*フォールバック*動作を有効にしました。これにより、ターゲットモジュールに__esModuleフラグが*あるが*デフォルトのエクスポートが*ない*場合に、不整合が生じました。トランスパイラとバンドラは、そのようなモジュールのデフォルトインポートをそのexports.defaultundefinedになります)にリンクし続けます。これは、実際のESMインポートはリンクできない場合にエラーが発生するため、TypeScriptでは理想的にはエラーになります。しかし、allowSyntheticDefaultImportsを使用すると、TypeScriptはそのようなインポートのデフォルトインポートがexportsオブジェクト全体にリンクしていると見なし、名前付きエクスポートをそのプロパティとしてアクセスできるようにします。
  2. allowSyntheticDefaultImportsは、名前空間インポートの型指定方法を変更しなかったため、両方が使用でき、同じ型を持つという奇妙な不整合が生じました。
    ts
    // @Filename: exportEqualsObject.d.ts
    declare const obj: object;
    export = obj;
    // @Filename: main.ts
    import objDefault from "./exportEqualsObject";
    import * as objNamespace from "./exportEqualsObject";
    // This should be true at runtime, but TypeScript gives an error:
    objNamespace.default === objDefault;
    // ^^^^^^^ Property 'default' does not exist on type 'typeof import("./exportEqualsObject")'.
  3. 最も重要なことは、allowSyntheticDefaultImportstscによって出力されるJavaScriptを変更しなかったことです。そのため、このフラグは、コードがBabelやWebpackなどの別のツールにフィードされる限り、より正確なチェックを可能にしましたが、--module commonjstscで出力し、Node.jsで実行しているユーザーに真の危険をもたらしました。 import *でエラーが発生した場合、allowSyntheticDefaultImportsを有効にすると修正されるように見えましたが、実際にはビルド時のエラーを抑制するだけで、Nodeでクラッシュするコードを出力していました。

TypeScriptは2.7でesModuleInteropフラグを導入しました。これは、TypeScriptの分析と既存のトランスパイラおよびバンドラで使用されている相互運用動作との間の残りの不整合に対処するためにインポートの型チェックを改善し、そして重要なことに、トランスパイラが数年前に採用したのと同じ__esModule条件付きCommonJS出力方法を採用しました。(import *の別の新しい出力ヘルパーにより、結果は常にオブジェクトであり、呼び出しシグネチャが削除され、前述の「非モジュールエンティティに解決される」エラーが完全に回避されないという仕様コンプライアンスの問題が完全に解決されました。)最後に、新しいフラグが有効になると、TypeScriptの型チェック、TypeScriptの出力、および残りのトランスパイルとバンドルのエコシステムは、仕様に準拠し、おそらくNodeで採用可能なCJS / ESM相互運用スキームに同意しました。

Node.jsでの相互運用

Node.jsは、v12でESモジュールをフラグなしでサポートするようになりました。バンドラやトランスパイラが数年前に始めたように、Node.jsはCommonJSモジュールにexportsオブジェクトの「合成デフォルトエクスポート」を提供し、ESMからのデフォルトインポートでモジュールコンテンツ全体にアクセスできるようにしました。

ts
// @Filename: export.cjs
module.exports = { hello: "world" };
// @Filename: import.mjs
import greeting from "./export.cjs";
greeting.hello; // "world"

これは、シームレスな移行のための1つの勝利です!残念ながら、類似点はほとんどそこで終わります。

__esModule検出なし(「二重デフォルト」問題)

Node.jsは、デフォルトのインポート動作を変更するために__esModuleマーカーを尊重することができませんでした。そのため、「デフォルトエクスポート」を持つトランスパイルされたモジュールは、別のトランスパイルされたモジュールによって「インポート」された場合とある方法で動作し、Node.jsの真のESモジュールによってインポートされた場合は別の方法で動作します。

ts
// @Filename: node_modules/dependency/index.js
exports.__esModule = true;
exports.default = function doSomething() { /*...*/ }
// @Filename: transpile-vs-run-directly.{js/mjs}
import doSomething from "dependency";
// Works after transpilation, but not a function in Node.js ESM:
doSomething();
// Doesn't exist after trasnpilation, but works in Node.js ESM:
doSomething.default();

トランスパイルされたデフォルトインポートは、ターゲットモジュールに__esModuleフラグがない場合にのみ合成デフォルトエクスポートを作成しますが、Node.jsは*常に*デフォルトエクスポートを合成し、トランスパイルされたモジュールに「二重デフォルト」を作成します。

信頼できない名前付きエクスポート

CommonJSモジュールのexportsオブジェクトをデフォルトインポートとして使用できるようにすることに加えて、Node.jsはexportsのプロパティを見つけて名前付きインポートとして使用できるようにしようとします。この動作は、機能する場合、バンドラとトランスパイラと一致します。ただし、Node.jsは構文解析を使用してコードが実行される前に名前付きエクスポートを合成しますが、トランスパイルされたモジュールは実行時に名前付きインポートを解決します。その結果、トランスパイルされたモジュールで機能するCJSモジュールからのインポートは、Node.jsでは機能しない可能性があります。

ts
// @Filename: named-exports.cjs
exports.hello = "world";
exports["worl" + "d"] = "hello";
// @Filename: transpile-vs-run-directly.{js/mjs}
import { hello, world } from "./named-exports.cjs";
// `hello` works, but `world` is missing in Node.js 💥
import mod from "./named-exports.cjs";
mod.world;
// Accessing properties from the default always works ✅

真のESモジュールをrequireできない

真のCommonJSモジュールは、実行時に両方がCommonJSであるため、ESMからCJSにトランスパイルされたモジュールをrequireできます。しかし、Node.jsでは、requireはESモジュールに解決されるとクラッシュします。これは、公開されたライブラリが、CommonJS(真またはトランスパイルされた)コンシューマーを壊すことなく、トランスパイルされたモジュールから真のESMに移行できないことを意味します。

ts
// @Filename: node_modules/dependency/index.js
export function doSomething() { /* ... */ }
// @Filename: dependent.js
import { doSomething } from "dependency";
// ✅ Works if dependent and dependency are both transpiled
// ✅ Works if dependent and dependency are both true ESM
// ✅ Works if dependent is true ESM and dependency is transpiled
// 💥 Crashes if dependent is transpiled and dependency is true ESM

異なるモジュール解決アルゴリズム

Node.jsは、require呼び出しを解決するための長年のアルゴリズムとは大きく異なる、ESMインポートを解決するための新しいモジュール解決アルゴリズムを導入しました。CJSとESモジュールの相互運用に直接関係はありませんが、この違いは、トランスパイルされたモジュールから真のESMへのシームレスな移行が不可能なもう1つの理由でした。

ts
// @Filename: add.js
export function add(a, b) {
return a + b;
}
// @Filename: math.js
export * from "./add";
// ^^^^^^^
// Works when transpiled to CJS,
// but would have to be "./add.js"
// in Node.js ESM.

結論

明らかに、トランスパイルされたモジュールからESMへのシームレスな移行は、少なくともNode.jsでは不可能です。これは私たちをどこに導くのでしょうか?

適切な module コンパイラオプションを設定することは非常に重要です。

ホストによって相互運用性のルールが異なるため、TypeScriptは、認識する各ファイルがどのような種類のモジュールを表し、どのようなルールセットを適用するかを理解しない限り、正しいチェック動作を提供できません。これが、module コンパイラオプションの目的です。(特に、Node.jsで実行することを意図したコードは、バンドラーによって処理されるコードよりも厳格なルールに従います。modulenode16 または nodenext に設定されていない限り、コンパイラの出力はNode.jsとの互換性についてチェックされません。)

CommonJSコードを使用するアプリケーションは、常にesModuleInteropを有効にする必要があります。

tscを使用してJavaScriptファイルを出力するTypeScript *アプリケーション*(他の人が利用する可能性のあるライブラリとは対照的に)では、esModuleInteropが有効になっているかどうかは大きな影響を与えません。特定の種類のモジュールに対するインポートの記述方法は変更されますが、TypeScriptのチェックと出力は同期されているため、エラーのないコードはどちらのモードでも安全に実行できます。この場合、esModuleInteropを無効のままにすることの欠点は、ECMAScript仕様に明らかに違反するセマンティクスを持つJavaScriptコードを記述できるようになり、名前空間のインポートに関する直感を混乱させ、将来ESモジュールの実行に移行することを困難にすることです。

一方、サードパーティのトランスパイラまたはバンドラーによって処理されるアプリケーションでは、esModuleInteropを有効にすることがより重要です。すべての主要なバンドラーとトランスパイラは、esModuleInteropのような出力戦略を使用しているため、TypeScriptはそれに合わせてチェックを調整する必要があります。(コンパイラは、tscが出力するJavaScriptファイルで何が起こるかについて常に推論するため、tscの代わりに別のツールが使用されている場合でも、出力に影響を与えるコンパイラオプションは、そのツールの出力とできるだけ一致するように設定する必要があります。)

esModuleInteropなしでallowSyntheticDefaultImportsを使用することは避けるべきです。これは、tscによって出力されるコードを変更せずにコンパイラのチェック動作を変更し、安全でないJavaScriptが出力される可能性があります。さらに、導入されるチェックの変更は、esModuleInteropによって導入される変更の不完全なバージョンです。tscが出力に使用されていない場合でも、allowSyntheticDefaultImportsよりもesModuleInteropを有効にする方が良いでしょう。

esModuleInteropが有効になっている場合、tscのJavaScript出力に含まれる__importDefaultおよび__importStarヘルパー関数の包含に反対する人もいます。それは、ディスク上の出力サイズがわずかに増加するため、またはヘルパーによって採用されている相互運用アルゴリズムが__esModuleをチェックすることによってNode.jsの相互運用動作を誤って表現しているように見えるためであり、前述の危険につながるためです。これらの反論は両方とも、esModuleInteropが無効になっている場合に示される欠陥のあるチェック動作を受け入れることなく、少なくとも部分的に対処できます。まず、importHelpersコンパイラオプションを使用して、ヘルパー関数を各ファイルにインライン化するのではなく、tslibからインポートできます。2番目の反論について説明するために、最後の例を見てみましょう。

ts
// @Filename: node_modules/transpiled-dependency/index.js
exports.__esModule = true;
exports.default = function doSomething() { /* ... */ };
exports.something = "something";
// @Filename: node_modules/true-cjs-dependency/index.js
module.exports = function doSomethingElse() { /* ... */ };
// @Filename: src/sayHello.ts
export default function sayHello() { /* ... */ }
export const hello = "hello";
// @Filename: src/main.ts
import doSomething from "transpiled-dependency";
import doSomethingElse from "true-cjs-dependency";
import sayHello from "./sayHello.js";

Node.jsで使用するCommonJSにsrcをコンパイルしているとします。allowSyntheticDefaultImportsまたはesModuleInteropがない場合、"true-cjs-dependency"からのdoSomethingElseのインポートはエラーであり、その他はエラーではありません。コンパイラオプションを変更せずにエラーを修正するには、インポートをimport doSomethingElse = require("true-cjs-dependency")に変更できます。ただし、モジュール(図示せず)の型の記述方法によっては、名前空間のインポートを記述して呼び出すこともできますが、これは言語レベルの仕様違反です。esModuleInteropを使用すると、表示されているインポートはすべてエラーではなく(すべて呼び出し可能です)、無効な名前空間のインポートがキャッチされます。

Node.jsでsrcを真のESMに移行することを決定した場合(たとえば、ルートpackage.jsonに"type": "module"を追加した場合)、何が変わるでしょうか?最初のインポートである"transpiled-dependency"からのdoSomethingは、もはや呼び出し可能ではなくなります。これは、「二重デフォルト」の問題を示しており、doSomething()ではなくdoSomething.default()を呼び出す必要があります。(TypeScriptは、--module node16およびnodenextでこれを理解してキャッチします。)しかし、注目すべきことに、CommonJSにコンパイルするときに機能するためにesModuleInteropを必要としたdoSomethingElseの* 2番目の*インポートは、真のESMで正常に機能します。

ここで不満を言うことがあるとすれば、それはesModuleInteropが2番目のインポートで行うことではありません。デフォルトのインポートを許可し、呼び出し可能な名前空間のインポートを防ぐという変更は、Node.jsの実際のESM / CJS相互運用戦略と完全に一致しており、実際のESMへの移行が容易になりました。問題があるとすれば、esModuleInteropが*最初の*インポートのシームレスな移行パスを提供できないように見えることです。しかし、この問題はesModuleInteropを有効にすることによって導入されたわけではありません。最初のインポートは、それによってまったく影響を受けませんでした。残念ながら、この問題は、main.tssayHello.tsの間のセマンティックコントラクトを破ることなく解決することはできません。なぜなら、sayHello.tsのCommonJS出力は、構造的にtranspiled-dependency/index.jsと同じに見えるからです。esModuleInteropdoSomethingのトランスパイルされたインポートの動作方法をNode.js ESMで動作する方法と同じになるように変更した場合、sayHelloインポートの動作も同じように変更され、入力コードがESMセマンティクスに違反します(したがって、変更なしにsrcディレクトリをESMに移行することはできません)。

見てきたように、トランスパイルされたモジュールから真のESMへのシームレスな移行パスはありません。しかし、esModuleInteropは正しい方向への一歩です。モジュール構文の変換とインポートヘルパー関数の包含を最小限に抑えたい人のために、esModuleInteropを無効にするよりもverbatimModuleSyntaxを有効にする方が良い選択です。verbatimModuleSyntaxは、CommonJS出力ファイルでimport mod = require("mod")およびexport = ns構文を使用することを強制し、真のESMへの移行の容易さを犠牲にして、説明したすべての種類のインポートのあいまいさを回避します。

ライブラリコードには特別な考慮事項が必要です。

ライブラリ(宣言ファイルを出荷するもの)は、作成する型が幅広いコンパイラオプションでエラーがないことを確認するために、特別な注意を払う必要があります。たとえば、あるインターフェースを別のインターフェースに拡張して、strictNullChecksが無効になっている場合にのみ正常にコンパイルされるように記述することができます。ライブラリがそのような型を公開する場合、すべてのユーザーもstrictNullChecksを無効にする必要があります。esModuleInteropを使用すると、型宣言に同様に「感染性の」デフォルトインポートを含めることができます。

ts
// @Filename: /node_modules/dependency/index.d.ts
import express from "express";
declare function doSomething(req: express.Request): any;
export = doSomething;

このデフォルトインポートがesModuleInteropが有効になっている場合にのみ機能し、そのオプションのないユーザーがこのファイルを参照するとエラーが発生するとします。ユーザーは*おそらく*とにかくesModuleInteropを有効にする必要がありますが、ライブラリがこのように構成を感染させるのは一般的に悪い形と見なされます。ライブラリが次のような宣言ファイルを出荷する方がはるかに良いでしょう。

ts
import express = require("express");
// ...

このような例から、ライブラリはesModuleInteropを有効にする*べきではない*という従来の知恵が生まれました。このアドバイスは妥当な出発点ですが、esModuleInteropを有効にすると、名前空間インポートの型が変更され、*エラーが発生する*可能性のある例を見てきました。そのため、ライブラリがesModuleInteropを有効または無効にしてコンパイルするかに関係なく、選択が感染する可能性のある構文を記述するリスクがあります。

最大限の互換性を確保するために上を行きたいライブラリ作成者は、コンパイラオプションのマトリックスに対して宣言ファイルを検証することをお勧めします。ただし、verbatimModuleSyntaxを使用すると、CommonJS出力ファイルでCommonJSスタイルのインポートおよびエクスポート構文を使用することを強制することにより、esModuleInteropに関する問題を完全に回避できます。さらに、esModuleInteropはCommonJSにのみ影響するため、時間の経過とともにESMのみの公開に移行するライブラリが増えるにつれて、この問題の関連性は低下します。

TypeScriptドキュメントはオープンソースプロジェクトです。プルリクエストを送信して、これらのページの改善にご協力ください

このページの投稿者
ABAndrew Branch (7)

最終更新日:2024年3月21日