モジュール - コンパイラオプションの選択

アプリを作成しています

単一のtsconfig.jsonは、利用可能なグローバル変数とモジュールの動作の両方に関して、単一の環境のみを表すことができます。アプリにサーバーコード、DOMコード、Webワーカーコード、テストコード、およびそれらすべてで共有されるコードが含まれている場合、それぞれに独自のtsconfig.jsonがあり、プロジェクト参照で接続されている必要があります。次に、tsconfig.jsonごとにこのガイドを1回使用します。アプリ内のライブラリのようなプロジェクト、特に複数のランタイム環境で実行する必要があるプロジェクトの場合は、「ライブラリを作成しています」セクションを使用してください。

バンドラーを使用しています

次の設定を採用することに加えて、今のところは、バンドラープロジェクトで{ "type": "module" }を設定したり、.mtsファイルを使用したりしないこともお勧めします。一部のバンドラーは、これらの状況下で異なるESM/CJS相互運用動作を採用しますが、TypeScriptは現在、"moduleResolution": "bundler"を使用して分析できません。詳細については、 issue#54102を参照してください。

json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Required
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
// Consult your bundler’s documentation
"customConditions": ["module"],
// Recommended
"noEmit": true, // or `emitDeclarationOnly`
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"verbatimModuleSyntax": true, // or `isolatedModules`
}
}

出力をコンパイルしてNode.jsで実行しています

ESモジュールを出力する場合は、"type": "module"を設定するか、.mtsファイルを使用してください。

json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Required
"module": "nodenext",
// Implied by `"module": "nodenext"`:
// "moduleResolution": "nodenext",
// "esModuleInterop": true,
// "target": "esnext",
// Recommended
"verbatimModuleSyntax": true,
}
}

ts-nodeを使用しています

ts-node は、Node.js で JS 出力をコンパイルして実行できるのと同じコードと tsconfig.json 設定との互換性を維持しようとしています。詳細については、ts-node のドキュメントを参照してください。

tsx を使用しています

ts-node はデフォルトで Node.js のモジュールシステムに最小限の変更を加えるのに対し、tsx はバンドラーのように動作し、拡張子なし/index モジュール指定子と ESM と CJS の任意の混在を許可します。tsx には、バンドラーに使用するのと同じ設定を使用してください。

バンドラーまたはモジュールコンパイラーを使用せずに、ブラウザー用の ES モジュールを作成しています

TypeScript にはこのシナリオ専用のオプションは現在ありませんが、`nodenext` ESM モジュール解決アルゴリズムと、URL およびインポートマップのサポートの代替としての `paths` を組み合わせて使用することで、近似できます。

json
// tsconfig.json
{
"compilerOptions": {
// This is not a complete template; it only
// shows relevant module-related settings.
// Be sure to set other important options
// like `target`, `lib`, and `strict`.
// Combined with `"type": "module"` in a local package.json,
// this enforces including file extensions on relative path imports.
"module": "nodenext",
"paths": {
// Point TS to local types for remote URLs:
"https://esm.sh/lodash@4.17.21": ["./node_modules/@types/lodash/index.d.ts"],
// Optional: point bare specifier imports to an empty file
// to prohibit importing from node_modules specifiers not listed here:
"*": ["./empty-file.ts"]
}
}
}

この設定により、明示的にリストされた HTTPS インポートはローカルにインストールされた型宣言ファイルを使用できますが、通常は node_modules で解決されるインポートではエラーが発生します。

ts
import {} from "lodash";
// ^^^^^^^^
// File '/project/empty-file.ts' is not a module. ts(2306)

あるいは、インポートマップを使用して、ブラウザでベア指定子のリストを URL に明示的にマップし、`nodenext` のデフォルトの node_modules ルックアップ、または `paths` に依存して、TypeScript をそれらのベア指定子インポートの型宣言ファイルに導くこともできます。

html
<script type="importmap">
{
"imports": {
"lodash": "https://esm.sh/lodash@4.17.21"
}
}
</script>
ts
import {} from "lodash";
// Browser: https://esm.sh/lodash@4.17.21
// TypeScript: ./node_modules/@types/lodash/index.d.ts

ライブラリを作成しています

ライブラリ作成者としてコンパイル設定を選択するプロセスは、アプリ作成者として設定を選択するプロセスとは根本的に異なります。アプリを作成する場合、ランタイム環境またはバンドラー(通常は動作が既知の単一のエンティティ)を反映した設定が選択されます。ライブラリを作成する場合、理想的には、*考えられるすべて*のライブラリコンシューマーのコンパイル設定でコードを確認する必要があります。これは現実的ではないため、代わりに、最も厳格な設定を使用できます。なぜなら、それらを満たせば、他のすべてを満たす傾向があるからです。

json
{
"compilerOptions": {
"module": "node16",
"target": "es2020", // set to the *lowest* target you support
"strict": true,
"verbatimModuleSyntax": true,
"declaration": true,
"sourceMap": true,
"declarationMap": true
}
}

これらの設定をそれぞれ選択した理由を調べてみましょう

  • **`module: "node16"`**。コードベースが Node.js のモジュールシステムと互換性がある場合、ほとんどの場合、バンドラーでも動作します。サードパーティのエミッターを使用して ESM 出力を生成する場合は、package.json で ` "type": "module" ` を設定して、TypeScript がコードを ESM としてチェックするようにしてください。ESM は、CommonJS よりも厳格なモジュール解決アルゴリズムを Node.js で使用します。例として、ライブラリが ` "moduleResolution": "bundler" ` でコンパイルされた場合に何が起こるかを見てみましょう。

    ts
    export * from "./utils";

    `./utils.ts` (または `./utils/index.ts`)が存在すると仮定すると、バンドラーはこのコードで問題ないため、` "moduleResolution": "bundler" ` はエラーを出しません。` "module": "esnext" ` でコンパイルすると、この export 文の出力 JavaScript は入力とまったく同じになります。その JavaScript が npm に公開された場合、バンドラーを使用するプロジェクトでは使用できますが、Node.js で実行するとエラーが発生します。

    Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/dependency/utils' imported from .../node_modules/dependency/index.js Did you mean to import ./utils.js?

    一方、次のように記述した場合

    ts
    export * from "./utils.js";

    これは、Node.js *と*バンドラーの両方で動作する出力を生成します。

    要するに、` "moduleResolution": "bundler" ` は伝染性があり、バンドラーでのみ動作するコードの生成を許可します。同様に、` "moduleResolution": "nodenext" ` は出力が Node.js で動作することのみをチェックしますが、ほとんどの場合、Node.js で動作するモジュールコードは他のランタイムおよびバンドラーで動作します。

  • **`target: "es2020"`**。この値をサポートする予定の*最も低い* ECMAScript バージョンに設定すると、生成されたコードで、後のバージョンで導入された言語機能が使用されないようになります。`target` は `lib` の対応する値も意味するため、古い環境では使用できない可能性のあるグローバルにアクセスすることもなくなります。

  • **`strict: true`**。これがなければ、出力 ` .d.ts `ファイルに含まれる型レベルのコードを作成し、コンシューマーが `strict` を有効にしてコンパイルするとエラーが発生する可能性があります。たとえば、この `extends` 句

    ts
    export interface Super {
    foo: string;
    }
    export interface Sub extends Super {
    foo: string | undefined;
    }

    は `strictNullChecks` の下でのみエラーになります。一方、`strict` が*無効*になっている場合にのみエラーになるコードを作成することは非常に難しいため、ライブラリは `strict` を使用してコンパイルすることを強くお勧めします。

  • **`verbatimModuleSyntax: true`**。この設定は、ライブラリコンシューマーに問題を引き起こす可能性のある、モジュール関連の落とし穴を防ぎます。第一に、ユーザーの `esModuleInterop` または `allowSyntheticDefaultImports` の値に基づいてあいまいな解釈が可能な import 文の記述を防ぎます。以前は、ライブラリで `esModuleInterop` を使用するとユーザーもそれを採用せざるを得なくなる可能性があるため、ライブラリは `esModuleInterop` なしでコンパイルすることがよく提案されていました。ただし、*`esModuleInterop` なしで*のみ動作するインポートを作成することもできるため、設定のどちらの値もライブラリの移植性を保証するものではありません。`verbatimModuleSyntax` はそのような保証を提供します。1 第二に、CommonJS として出力されるモジュールで `export default` を使用することを防ぎます。これは、バンドラーユーザーと Node.js ESM ユーザーがモジュールを異なる方法で消費することを要求する可能性があります。詳細については、ESM/CJS 相互運用性に関する付録を参照してください。

  • **`declaration: true`** は、出力 JavaScript と一緒に型宣言ファイルを生成します。これは、ライブラリのコンシューマーが型情報を持つために必要です。

  • **`sourceMap: true`** および **`declarationMap: true`** は、それぞれ出力 JavaScript および型宣言ファイルのソースマップを生成します。これらは、ライブラリがソース(`.ts`)ファイルも配布する場合にのみ役立ちます。ソースマップとソースファイルを配布することにより、ライブラリのコンシューマーはライブラリコードをいくぶん簡単にデバッグできるようになります。宣言マップとソースファイルを配布することにより、コンシューマーはライブラリからのインポートで定義へ移動を実行するときに、元の TypeScript ソースを見ることができるようになります。これらの両方は、開発エクスペリエンスとライブラリサイズの間のトレードオフを表すため、それらを含めるかどうかはあなた次第です。

ライブラリのバンドルに関する考慮事項

バンドラーを使用してライブラリを生成する場合、すべての(外部化されていない)インポートは、ユーザーの未知の環境ではなく、既知の動作を持つバンドラーによって処理されます。この場合、` "module": "esnext" ` と ` "moduleResolution": "bundler" ` を使用できますが、次の 2 つの注意点があります。

  1. 一部のファイルがバンドルされ、一部が外部化されている場合、TypeScript はモジュール解決をモデル化できません。依存関係を持つライブラリをバンドルする場合、ファーストパーティライブラリのソースコードを単一のファイルにバンドルしますが、外部依存関係のインポートはバンドルされた出力の実際のインポートとして残すのが一般的です。これは本質的に、モジュール解決がバンドラーとエンドユーザーの環境に分割されることを意味します。TypeScript でこれをモデル化するには、バンドルされたインポートを ` "moduleResolution": "bundler" ` で、外部化されたインポートを ` "moduleResolution": "nodenext" ` で(または複数のオプションを使用してすべてのエンドユーザー環境で動作することを確認する)処理します。ただし、TypeScript は、同じコンパイルで 2 つの異なるモジュール解決設定を使用するように設定することはできません。結果として、` "moduleResolution": "bundler" ` を使用すると、バンドラーでは動作するが Node.js では安全ではない、外部化された依存関係のインポートが許可される可能性があります。一方、` "moduleResolution": "nodenext" ` を使用すると、バンドルされたインポートに過度に厳格な要件が課される可能性があります。

  2. 宣言ファイルもバンドルされていることを確認する必要があります。宣言ファイルの最初のルールを思い出してください。すべての宣言ファイルは、正確に 1 つの JavaScript ファイルを表します。` "moduleResolution": "bundler" ` を使用し、バンドラーを使用して ESM バンドルを生成し、`tsc` を使用して多くの個別の宣言ファイルを生成する場合、宣言ファイルは ` "module": "nodenext" ` で使用されるとエラーが発生する可能性があります。たとえば、次のような入力ファイル

    ts
    import { Component } from "./extensionless-relative-import";

    は、JS バンドラーによってインポートが消去されますが、同一の import 文を含む宣言ファイルが生成されます。ただし、その import 文には、ファイル拡張子が欠落しているため、Node.js では無効なモジュール指定子が含まれます。Node.js ユーザーの場合、TypeScript は宣言ファイルでエラーを出し、依存関係がランタイムでクラッシュすると仮定して、`Component` を参照する型に `any` を感染させます。

    TypeScript バンドラーがバンドルされた宣言ファイルを生成しない場合は、` "moduleResolution": "nodenext" ` を使用して、宣言ファイルに保持されているインポートがエンドユーザーの TypeScript 設定と互換性があることを確認してください。さらに良いのは、ライブラリをバンドルしないことを検討することです。

デュアルエミットソリューションに関する注意事項

単一の TypeScript コンパイル(出力の有無にかかわらず、型チェックのみの場合も含む)では、各入力ファイルが 1 つの出力ファイルのみを生成すると想定されています。tsc が何も出力しない場合でも、インポートされた名前に対して実行される型チェックは、tsconfig.json に設定されているモジュール関連および出力関連のオプションに基づいて、出力ファイルが実行時にどのように動作するかについての知識に依存します。サードパーティのエミッターは、tsc が他のエミッターの出力を理解できるように設定されている限り、一般的に tsc の型チェックと組み合わせて安全に使用できますが、1 回の型チェックで異なるモジュール形式の 2 つの異なる出力セットを出力するソリューションは、(少なくとも)一方の出力を未チェックのままにします。外部依存関係は CommonJS と ESM のコンシューマーに異なる API を公開する可能性があるため、単一のコンパイルで両方の出力が型セーフであることを保証するために使用できる設定はありません。実際には、ほとんどの依存関係はベストプラクティスに従っており、デュアル出力は機能します。公開前にすべての出力バンドルに対してテストと 静的解析 を実行することで、深刻な問題が見過ごされる可能性が大幅に減少します。


  1. verbatimModuleSyntax は、JS エミッターが tsconfig.json、ソースファイルの拡張子、および package.json の "type" を考慮して tsc が出力するのと同じモジュール種類を出力する場合にのみ機能します。このオプションは、記述された import/require が、出力される import/require と同一であることを強制することで機能します。同じソースファイルから ESM と CJS の両方の出力を生成する設定は、verbatimModuleSyntax と根本的に互換性がありません。これは、require が出力される場所に import を記述することを防ぐことが目的だからです。verbatimModuleSyntax は、サードパーティのエミッターが tsc とは異なるモジュール種類を出力するように設定することによっても無効にすることができます。たとえば、tsconfig.json で "module": "esnext" を設定しながら、Babel を CommonJS を出力するように設定することなどです。

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

このページの貢献者
ABAndrew Branch (6)

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