TypeScriptにおけるモジュールシステムは、長年開発者を悩ませてきた複雑なテーマの一つです。
JavaScriptの歴史的な背景から、Node.jsを中心に普及したCommonJS (CJS)と、モダンなブラウザや最新のランタイムが標準として採用するECMAScript Modules (ESM)の2つが共存しているためです。
2026年現在、多くのプロジェクトがESMへの移行を完了させていますが、既存のライブラリ資産や特定の実行環境においては、依然としてCJSとの互換性を考慮する必要があります。
本記事では、TypeScriptにおけるこれら2つのモジュールシステムの仕組みを解き明かし、現代の開発において最も推奨される設定手法について詳しく掘り下げていきます。
モジュールシステムの基礎知識:CJSとESMの違い
TypeScriptで適切な設定を行うためには、まず基盤となるJavaScriptのモジュールシステムについて正確に理解しておく必要があります。
モジュールシステムとは、コードを独立したファイルに分割し、必要に応じてそれらを再利用するための仕組みです。
CommonJS (CJS) の特徴と役割
CommonJSは、主にNode.jsの初期から採用されていたモジュールシステムです。
コード内では require() 関数を使ってモジュールを読み込み、module.exports を使って値を外部に公開します。
// mathUtils.cts (TypeScriptにおけるCJSの例)
const add = (a: number, b: number): number => {
return a + b;
};
// モジュールのエクスポート
module.exports = { add };
// 別のファイルでのインポート
// const { add } = require('./mathUtils');
CJSの大きな特徴は、同期的に読み込みが行われる点です。
実行時にファイルシステムをスキャンしてモジュールをロードするため、サーバーサイドでの動作には適していましたが、ブラウザのようなネットワーク経由での読み込みが必要な環境では非効率となるケースがありました。
ECMAScript Modules (ESM) の台頭
ESMはJavaScriptの標準仕様(ECMAScript)として定義されたモジュールシステムです。
import と export というキーワードを使用します。
2026年現在のモダンなフロントエンド開発およびNode.js開発において、ESMは標準的な選択肢となっています。
// mathUtils.mts (TypeScriptにおけるESMの例)
export const add = (a: number, b: number): number => {
return a + b;
};
// 別のファイルでのインポート
// import { add } from './mathUtils.mjs';
ESMの利点は、静的解析が可能であることです。
ビルドツールはコードを実行することなく依存関係を把握できるため、未使用のコードを削除する「ツリーシェイキング」などの最適化が容易になります。
また、トップレベルでの await が使用できるなど、現代的なJavaScriptの機能をフルに活用できます。
TypeScriptにおけるモジュール設定の重要性
TypeScriptはJavaScriptにコンパイルされる言語であるため、TypeScriptコンパイラ(tsc)が「どのような形式でJavaScriptを出力するか」を指示する必要があります。
これに関わるのが tsconfig.json 内の module 設定です。
tsconfig.jsonにおける主要なオプション
TypeScriptの挙動を制御する設定項目は多岐にわたりますが、モジュールシステムに関連する最重要項目は以下の3つです。
| 設定項目 | 役割 |
|---|---|
module | 出力されるJavaScriptのモジュール形式を指定する。 |
moduleResolution | コンパイラがインポートパスをどのように解決するかを指定する。 |
target | 出力されるJavaScriptの構文レベル(ES2020, ESNextなど)を指定する。 |
以前のTypeScriptでは module: "commonjs" や module: "esnext" が多用されていましたが、2026年現在、Node.js環境での開発においては NodeNext または Node16 を指定することが推奨されています。
module: NodeNext の意味
NodeNext を指定すると、TypeScriptはNode.jsの最新のモジュール解決ルールに従います。
これには、後述する package.json の type フィールドの参照や、拡張子(.mts, .cts)による挙動の変化が含まれます。
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true
}
}
この設定を行うことで、TypeScriptはプロジェクト全体がESMなのかCJSなのかを動的に判断し、適切な型チェックとコード生成を行うようになります。
Node.jsにおけるESMとCJSの共存ルール
TypeScriptプロジェクトを運営する上で避けて通れないのが、Node.jsのモジュール決定ルールです。
Node.jsは、ファイルがESMとして扱われるかCJSとして扱われるかを、いくつかの条件で決定します。
package.json の type フィールド
プロジェクト全体をESMとして扱いたい場合、package.json に "type": "module" を記述します。
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"dependencies": {
"typescript": "^5.x.x"
}
}
この設定があるディレクトリ内では、拡張子が .ts(または .js)のファイルはすべて ESM として扱われます。
逆に、この設定がない場合、デフォルトでは CJS として扱われます。
拡張子による明示的な指定
プロジェクトの設定に関わらず、特定のファイルだけモジュールシステムを固定したい場合があります。
そのために、TypeScript(およびNode.js)では特別な拡張子が用意されています。
.mts(TypeScript) /.mjs(JavaScript): 常にESMとして扱われる.cts(TypeScript) /.cjs(JavaScript): 常にCJSとして扱われる
これにより、ESMベースのプロジェクトの中に、どうしてもCJSで記述しなければならないスクリプトを混在させることが可能になります。
インポート時の「拡張子」問題
TypeScriptでESMを扱う際、最も多くの開発者がつまずくポイントが インポートパスの記述 です。
ESM仕様およびNode.jsのモダンな解決ルールでは、インポートパスに 拡張子を含めることが必須 となっています。
.js 拡張子を付けてインポートする理由
驚くべきことに、TypeScriptファイル(.ts)をインポートする場合でも、コード上では .js 拡張子を付けて記述する必要があります。
// user.ts
export const user = { name: "Tanaka" };
// main.ts
// コンパイル後のJavaScriptでも動作するように、.jsを指定する
import { user } from './user.js';
console.log(user.name);
TypeScriptコンパイラは、この .js を「対応するソースファイル(.ts)」として正しく認識し、型チェックを行います。
これは、コンパイル後のJavaScriptコードが、ブラウザやNode.jsでそのまま動作することを保証するための仕様です。
2026年現在では、ビルドツール(ViteやWebpackなど)がこれを自動補完してくれるケースも多いですが、TypeScript本来の動作としては拡張子の明示が基本です。
ESMとCJSの相互運用(Interoperability)
プロジェクト内でESMとCJSが混在する場合、いくつかの制約が発生します。
特に「ESMからCJSを呼ぶ」のと「CJSからESMを呼ぶ」のでは難易度が大きく異なります。
ESMからCJSを呼び出す
ESM環境からCJSライブラリを読み込むのは比較的容易です。
多くの場合、デフォルトインポートを使用してアクセスできます。
// ESM環境
import pkg from 'cjs-library';
// CJSの module.exports が pkg として取得できる
ただし、CJS側が名前付きエクスポートを正しく解釈できない場合があるため、その際はデストラクトを行わずに一度オブジェクトとして受け取ることが推奨されます。
CJSからESMを呼び出す
これは非常に困難なパターンです。
require() は同期処理ですが、ESMは非同期的にロードされる可能性があるため、CJSから ESM を require することはできません。
どうしても必要な場合は、動的インポート(Dynamic Import)を使用します。
// CJS環境
async function loadApp() {
const { someFunction } = await import('./esm-module.mjs');
someFunction();
}
このように、モジュールシステム間の障壁を理解しておくことは、ライブラリの選定やアーキテクチャ設計において極めて重要です。
ライブラリ開発における「Dual Packages」の構築
自分が作成したライブラリを公開する場合、利用者がESM環境であってもCJS環境であっても動作するようにすることが理想的です。
これを Dual Packages と呼びます。
exports フィールドの活用
package.json の exports フィールドを使用すると、条件に応じてエントリーポイントを切り替えることができます。
{
"name": "my-library",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs"
}
}
}
このように設定することで、利用者が import した場合はESM版が、require した場合はCJS版が自動的に選択されます。
TypeScript側では、それぞれの形式に合わせてコンパイル結果を出力するようにビルドパイプラインを構築する必要があります。
現代的な開発における推奨設定パターン
2026年の標準的なプロジェクトにおいて、トラブルを最小限に抑えつつパフォーマンスを最大化するための設定パターンを整理します。
新規アプリケーション開発の場合
基本的には フルESM で構成することを強く推奨します。
package.jsonに"type": "module"を設定。tsconfig.jsonのmoduleとmoduleResolutionを"NodeNext"に設定。- インポートパスには必ず拡張子(.js)を付与する。
この構成にすることで、最新のライブラリ(Pure ESMなライブラリ)の恩恵をフルに受けられ、ビルドツールの最適化も効きやすくなります。
既存の大規模プロジェクトの場合
古いCJSベースのプロジェクトを移行する場合、一気に変更するのはリスクが高いため、段階的な移行を検討してください。
- 新規作成する機能だけを
.mtsで記述し、ESMとして実装する。 - 共通の設定ファイルなどは
.ctsとして維持する。 moduleResolution: "bundler"を活用し、Viteなどのモダンなツールに解決を任せる。
モジュール解決にまつわるトラブルシューティング
開発中に発生しやすい典型的なエラーとその対処法を紹介します。
エラー:ReferenceError: exports is not defined
これは、ESM環境として動作しているファイル内で、CJS独自の変数である exports や module を使用しようとした際に発生します。
対処法: コードを export 構文に書き換えるか、ファイルの拡張子を .cts に変更してCJSとして明示します。
エラー:Unknown file extension “.ts”
Node.jsでTypeScriptファイルを直接実行しようとした際、Node.jsがそのファイルを解釈できない場合に発生します。
対処法: tsx や ts-node などの実行ツールを使用するか、コンパイル後のJavaScriptファイルを実行するようにしてください。
エラー:Module not found(拡張子の欠落)
ESM設定下で、拡張子を省略してインポートした場合に発生します。
対処法: インポート文を import { func } from './file.js' のように修正します。
まとめ
TypeScriptにおけるモジュールシステムは、JavaScriptの進化の歴史を色濃く反映した複雑な仕組みです。
しかし、2026年現在のベストプラクティスは明確になりつつあります。
ESMを標準として採用し、NodeNextの設定を通じてNode.jsの解決ルールに準拠することが、最も将来性が高くトラブルの少ない道です。
CJSとの互換性は依然として重要ですが、それはインフラとしての理解に留め、日々のコーディングではESMの強力な静的解析と最適化能力を活用すべきです。
本記事で解説した tsconfig.json の設定や拡張子の扱い、そして package.json による制御を適切に組み合わせることで、堅牢でメンテナンス性の高いTypeScriptプロジェクトを構築できるでしょう。
モジュールシステムの深い理解は、単なるエラー解消に留まらず、アプリケーション全体のパフォーマンス向上や適切なライブラリ設計にも直結する重要なスキルです。
