はじめに
TypeScript を使っていて「モジュールのインポートがうまくいかない...」「パスの解決エラーが出る...」と困ったことはありませんか?
特に tsconfig.json の moduleResolution 設定は、モジュールの解決方法を決定する重要な設定ですが、その挙動の違いが分かりにくいですよね。
この記事では、実際のサンプルコードを使いながら、moduleResolution の各設定値の役割と動作の違いを整理します。
開発環境
開発環境は以下の通りです。
- Windows11
- TypeScript 5.9.2
- Node.js 22.18.0
- npm 11.5.2
結論
moduleResolution は「TypeScript がモジュールをどのように探すか」を決定する設定です。
| 設定値 | 説明 | 主な用途 |
|---|---|---|
node10 (旧: node) |
Node.js の従来の解決方式 | CommonJS プロジェクト |
node16 |
Node.js 12+ の ESM/CJS 両対応 | 現代的な Node.js プロジェクト |
nodenext |
最新の Node.js 解決方式 | 推奨設定 |
bundler |
bundler 前提の解決方式 | Vite、webpack などを使う場合 |
moduleResolution とは?
moduleResolution は、TypeScript がインポート文を解析する際に「どのファイルを読み込むべきか」を決定するアルゴリズムを指定します。
例えば、以下のようなインポート文があるとき:
import { add } from "./math";
import express from "express";
TypeScript は以下を決定する必要があります:
-
"./math"→ どのファイルを指すのか?(math.ts?math.tsx?math/index.ts?) -
"express"→node_modulesのどこを見るのか? - 拡張子の省略は許可されるのか?
この解決方法を決めるのが moduleResolution です。
プロジェクトの準備
以下のディレクトリ構成で動作を確認します。
project/
├── src/
│ ├── utils/
│ │ ├── math.ts
│ │ └── index.ts
│ ├── helpers/
│ │ └── logger.mjs
│ └── main.ts
├── package.json
└── tsconfig.json
サンプルコード
src/utils/math.ts
export const add = (a: number, b: number): number => a + b;
export const multiply = (a: number, b: number): number => a * b;
src/utils/index.ts
export { add, multiply } from "./math";
export const version = "1.0.0";
src/helpers/logger.mjs
export const log = (message: string) => {
console.log(`[LOG] ${message}`);
};
src/main.ts
// ケース1: 拡張子なしの相対パス
import { add } from "./utils/math";
// ケース2: ディレクトリインポート
import { version } from "./utils";
// ケース3: .mjs ファイルのインポート
import { log } from "./helpers/logger.mjs";
// ケース4: node_modules からのインポート
import express from "express";
log(`Version: ${version}`);
console.log(add(2, 3));
1. moduleResolution: "node10" (旧: "node")
従来の Node.js スタイルのモジュール解決方式です。
設定
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node10"
}
}
動作の特徴
拡張子の自動補完
import { add } from "./utils/math" は以下の順序でファイルを探します:
./utils/math.ts./utils/math.tsx./utils/math.d.ts
拡張子を省略しても TypeScript が自動的に補完してくれます。
ディレクトリインデックス解決
import { version } from "./utils" は以下の順序でファイルを探します:
./utils.ts./utils.tsx./utils.d.ts./utils/index.ts./utils/index.tsx./utils/index.d.ts
package.json の types フィールド
node_modules 内のパッケージは package.json の types または typings フィールドを参照します。
{
"name": "express",
"main": "index.js",
"types": "index.d.ts"
}
制限事項
-
.mjsファイルのインポート時に拡張子の記述が必要 - ESM と CJS の区別が曖昧
-
package.jsonのexportsフィールドに非対応
2. moduleResolution: "node16" / "nodenext"
Node.js 12+ の ESM サポートに対応した解決方式です。
設定
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
Note:
node16とnodenextはほぼ同じですが、nodenextは将来的な Node.js の変更に追従します。
動作の特徴
package.json による ESM/CJS の判定
プロジェクトのルートまたは最寄りの package.json を確認し、"type" フィールドで解決方式を決定します。
package.json
{
"type": "module"
}
-
"type": "module"→ ESM として扱う -
"type": "commonjs"または未指定 → CJS として扱う
相対インポートでの拡張子必須
ESM モードでは、相対インポートに拡張子が必須になります。
// ❌ エラー: 拡張子がない
import { add } from "./utils/math";
// ✅ OK: 拡張子を明示
import { add } from "./utils/math.js";
重要: TypeScript のコードでも
.js拡張子を書きます(.tsではありません)。これは、コンパイル後の JavaScript ファイルを指すためです。
ディレクトリインデックスの非サポート
ESM モードでは、ディレクトリインポートが使えません。
// ❌ エラー
import { version } from "./utils";
// ✅ OK: index.js を明示
import { version } from "./utils/index.js";
package.json の exports フィールド対応
node_modules 内のパッケージは exports フィールドを尊重します。
{
"name": "my-package",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"types": "./dist/utils.d.ts"
}
}
}
実際のコード例
package.json
{
"type": "module",
"dependencies": {
"express": "^4.18.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "./dist"
}
}
src/main.ts
// 拡張子を明示(.js を記述)
import { add, multiply } from "./utils/math.js";
import { version } from "./utils/index.js";
import { log } from "./helpers/logger.mjs";
log(`Version: ${version}`);
console.log(add(2, 3));
console.log(multiply(4, 5));
コンパイル結果
dist/main.js
import { add, multiply } from "./utils/math.js";
import { version } from "./utils/index.js";
import { log } from "./helpers/logger.mjs";
log(`Version: ${version}`);
console.log(add(2, 3));
console.log(multiply(4, 5));
拡張子がそのまま保持され、Node.js の ESM として正しく動作します。
3. moduleResolution: "bundler"
Vite、webpack、esbuild などのバンドラーを使用することを前提とした解決方式です。
設定
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
動作の特徴
拡張子の省略が可能
nodenext と異なり、ESM モードでも拡張子を省略できます。
// ✅ OK: 拡張子なしでも動作
import { add } from "./utils/math";
import { version } from "./utils";
これは、バンドラーが自動的に拡張子を解決してくれるためです。
ディレクトリインデックスのサポート
// ✅ OK: ディレクトリインポートが可能
import { version } from "./utils";
./utils/index.ts が自動的に解決されます。
package.json の exports 対応
nodenext と同様に、package.json の exports フィールドを尊重します。
TypeScript 固有の拡張子
.ts、.tsx のインポートも許可されます(バンドラーが処理するため)。
// ✅ OK: バンドラー使用時のみ
import { add } from "./utils/math.ts";
いつ使うべきか
- Vite、webpack、Rollup などのバンドラーを使用する場合
- フロントエンド開発(React、Vue など)
- バンドラーが拡張子やパスの解決を行う環境
実際のコード例
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"]
}
}
src/main.ts
// 拡張子省略OK
import { add, multiply } from "./utils/math";
import { version } from "./utils";
console.log(`Version: ${version}`);
console.log(add(2, 3));
console.log(multiply(4, 5));
// DOM操作もOK
document.getElementById("app")!.textContent = `Result: ${add(10, 20)}`;
この設定は、Vite などのバンドラーが適切にファイルを解決してくれることを前提としています。
各設定値の比較
| 特徴 | node10 | node16/nodenext | bundler |
|---|---|---|---|
| 拡張子省略 | ✅ 可能 | ❌ 不可(ESMモード) | ✅ 可能 |
| ディレクトリインデックス | ✅ 可能 | ❌ 不可(ESMモード) | ✅ 可能 |
| package.json "type" 認識 | ❌ なし | ✅ あり | ✅ あり |
| package.json "exports" | ❌ 非対応 | ✅ 対応 | ✅ 対応 |
| .mjs/.cjs の区別 | △ 限定的 | ✅ 完全対応 | ✅ 完全対応 |
| 用途 | 従来のNode.js | 現代的なNode.js | バンドラー使用時 |
実践的な使い分け
Node.js バックエンド開発
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
現代的な Node.js プロジェクトでは nodenext が推奨されます。
フロントエンド開発(Vite/webpack使用)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"]
}
}
バンドラーを使用する場合は bundler が便利です。
レガシープロジェクト
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node10"
}
}
既存の CommonJS プロジェクトでは node10 を維持することも選択肢です。
よくあるエラーと対処法
エラー1: "Cannot find module './math' or its corresponding type declarations"
原因: nodenext では拡張子が必須です。
// ❌ エラー
import { add } from "./math";
// ✅ 修正
import { add } from "./math.js";
エラー2: "Directory import './utils' is not supported"
原因: nodenext ではディレクトリインポートが使えません。
// ❌ エラー
import { version } from "./utils";
// ✅ 修正
import { version } from "./utils/index.js";
エラー3: "The current file is a CommonJS module"
原因: package.json の "type" 設定とファイル拡張子の不一致。
対処法:
- ESM を使いたい場合:
package.jsonに"type": "module"を追加 - CJS を使いたい場合:
module: "commonjs"とmoduleResolution: "node10"を使用
まとめ
moduleResolution は TypeScript のモジュール解決方法を決定する重要な設定です。
- node10(旧: node): 従来の Node.js スタイル。拡張子省略やディレクトリインデックスが可能ですが、現代的な ESM の機能には非対応
-
node16/nodenext: Node.js の ESM/CJS を正確にサポート。拡張子の明示が必要ですが、
package.jsonのexportsなどに対応し、現代的な Node.js 開発に最適です - bundler: バンドラー使用を前提とした設定。拡張子省略が可能で、フロントエンド開発に便利
プロジェクトの実行環境(Node.js 直接実行 or バンドラー経由)に合わせて、適切な moduleResolution を選択することで、TypeScript のモジュール解決をスムーズに行えます。