こんにちは!seiです!
Node.js×typescriptでプロジェクトを作成する際に、module形式の違いによる設定方法が分からず、はまったので備忘録です!
今回はNode.jsプロジェクトの根幹となる`package.json`と`tsconfig.json`の設定について、その歴史的背景から最新のベストプラクティスまで、徹底的に解説します。特にCommonJSとESM(ECMAScript Modules)の違いに焦点を当て、プロジェクト設計に直結する重要な設定フィールドの意味を詳しく説明します。
ESM CommonJSとは?
簡単に言うと、JavaScriptのモジュールシステムの種類です。
npmで管理しているパッケージやファイルをどんな書き方で読み込むのかの違いです。
なぜモジュールシステムが複数あるのかというと以下のような歴史的背景があります。
フロントのJavascriptもNode.jsもESMに統一されつつあるようです。
ブラウザJavaScriptの初期とモジュール化の課題
JavaScriptは当初、小さなスクリプト言語として設計され、モジュールシステムを持っていませんでした。スクリプトは単一のグローバルスコープで実行され、変数の衝突や依存関係の管理が大きな課題でした。
<!-- 古いウェブサイトでよく見られた実装方法 --> <script src="jquery.js"></script> <script src="plugin1.js"></script> <!-- jQueryに依存 --> <script src="app.js"></script> <!-- 両方に依存 -->
この問題に対処するため、様々なモジュールパターンが生まれました:
– IIFE(即時実行関数式)パターン
– AMD(Asynchronous Module Definition):RequireJSなど
– UMD(Universal Module Definition):複数環境対応
CommonJSの誕生とNode.jsへの採用
2009年、サーバーサイドJavaScriptのためのCommonJSプロジェクトが始まり、Node.jsはこのモジュールシステムを採用しました。
CommonJSの特徴:
- サーバーサイド向けに設計された同期的なモジュール読み込み
- `require()`と`module.exports`による明確なAPI
- ブラウザでは直接使用できず、Browserifyなどのツールが必要
// math.js (CommonJS)
const add = (a, b) => a + b;
module.exports = { add };
// app.js (CommonJS)
const math = require('./math');
console.log(math.add(1, 2)); // 3
ECMAScript Modulesの標準化
ES2015(ES6)でJavaScriptに公式のモジュールシステムが導入されました
- 言語仕様の一部として標準化
- 静的インポート構文で静的解析が可能
- 非同期読み込みを前提とした設計
// math.js (ESM)
export const add = (a, b) => a + b;
// app.js (ESM)
import { add } from './math.js';
console.log(add(1, 2)); // 3
Node.jsでは、v12から実験的にESMをサポートし始め、v14以降で本格的にサポートされるようになりました。
package.jsonの設定
モジュールタイプを決定する「type」フィールド
ESMを使いたい場合、package.jsonにはtypeを必ず指定してください。
{
"type": "module" // または "commonjs"(省略時のデフォルト)
}
– type: “commonjs”(デフォルト):`.js`ファイルはCommonJSとして扱われる
– type: “module”:`.js`ファイルはESMとして扱われる
🔑 重要ポイント:この設定は、`import`/`export`または`require`/`module.exports`のどちらの構文を使うべきかを決定します。不一致があるとランタイムエラーになります。
ファイル拡張子でモジュールタイプを明示的に指定することも可能です:
– `.mjs`:常にESMとして扱われる
– `.cjs`:常にCommonJSとして扱われる
モジュールエントリポイントの定義
以下の設定はローカルプロジェクト内で使うだけなら必須ではありません。
作ったパッケージを外部公開する際や他のプロジェクトから読み込みたい場合に設定するものです。
{
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
}
}
}
– main:外部からNode.jsやCommonJSで`require()`を使ってパッケージをインポートした時のエントリポイント
– module:ESMに対応したバンドラー(webpack, Rollup等)が優先的に使用するエントリポイント
– types:TypeScriptの型定義ファイルの場所
exportsフィールドはNode.js 12.7.0以降で導入された強力な機能で:
– パッケージのパブリックAPIを明示的に定義
– インポート方法(ESM vs CommonJS)によって異なるファイルを提供
– サブパス(`./utils`など)のエクスポートも細かく制御可能
– エクスポートされていないパスへのアクセスを禁止(セキュリティ向上)
tsconfig.jsonの詳細設定
どちらのモジュールシステムを使用してJavaScriptにトランスパイルするか判断できないため、typescriptの設定も行う必要があります。
モジュール関連の設定
{
"compilerOptions": {
"module": "ESNext", // または "CommonJS", "AMD", "UMD", "System" など
"moduleResolution": "node", // または "node16", "nodenext", "classic"
"target": "ES2020", // コンパイル後のJSバージョン
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
module設定の詳細
TypeScriptファイルをコンパイルした際に生成されるJavaScriptのモジュールシステムを指定:
– “CommonJS”:`require()`と`module.exports`を使用するコードを生成
– “ESNext”:最新のECMAScript Modulesの機能を含むコードを生成
– “ES2020”:ES2020仕様のモジュール構文を使用
🔑 重要ポイント:この設定は、`package.json`の`type`フィールドと整合性を保つ必要があります:
– `type: “commonjs”`なら`module: “CommonJS”`
– `type: “module”`なら`module: “ESNext”`か`module: “ES2020″`など(ESNextなら最新の構文)
moduleResolution設定
モジュール解決方法を指定する重要な設定:
– “node”:Node.jsの伝統的なモジュール解決アルゴリズムを使用
– “node16″/”nodenext”:Node.js 16以降のESMとCommonJSの両方をサポートする解決方法
– “classic”:古いTypeScriptの解決方法(非推奨)
パス解決に関する設定
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"rootDir": "./src",
"outDir": "./dist"
}
}
– baseUrl:非相対的インポートの基準となるディレクトリ
– paths:モジュールのエイリアスを定義(例:`@/components/Button`→`src/components/Button`)
– rootDir:入力ファイルの最上位ディレクトリ
– outDir:出力ファイルの配置先ディレクトリ
ESMとCommonJSはどちらを使えばよい?選択基準は?
モジュールシステムを選択する際の考慮点
モジュールシステムを選択する際の考慮点は以下かなと思います。
1. 互換性:
– 使用するライブラリがどのモジュールシステムをサポートしているか?
– 特に古いライブラリはCommonJSのみをサポートしている場合があるときは注意
2. 環境のサポート:
– Node.jsのバージョン(v12より前はESMの完全なサポートなし)
– ビルドツールの対応状況
3. 機能要件:
– 動的インポート(`import()`)の必要性
– トップレベルの`await`の使用
// ESMでのみ可能なトップレベルのawait
// app.js (ESM)
const data = await fetch('https://api.example.com/data');
console.log(await data.json());
// CommonJSでは関数内に記述する必要がある
// app.js (CommonJS)
async function main() {
const data = await fetch('https://api.example.com/data');
console.log(await data.json());
}
main();
動的モジュール読み込みの違い
動的にモジュールを読み込む場合に違いがあります。
ESMの場合は必ず非同期で読み込まなければなりません。
// CommonJSでの動的モジュール読み込み
function loadModule(condition) {
if (condition) {
return require('./module-a');
} else {
return require('./module-b');
}
}
// ESMでも動的インポートは可能だが非同期
async function loadModule(condition) {
if (condition) {
return await import('./module-a.js');
} else {
return await import('./module-b.js');
}
}
使用シーン | 選ぶべき方法 |
---|---|
Node.js 環境で同期処理したい | require() (CommonJS) |
モダンなJSで非同期に読み込みたい | import() (ESM) |
ブラウザでコードを動的にロードしたい | import() 一択 |
まとめ:最新のベストプラクティス
Node.js開発における現在のベストプラクティスをまとめると:
1. 新規プロジェクト:
– 特に制約がなければESMを選択
– TypeScriptを使用する場合は、`”module”: “NodeNext”`と`”moduleResolution”: “NodeNext”`を設定
2. パッケージ公開時:
– `exports`フィールドを使用して両方のモジュールシステムをサポート
– TypeScriptの型定義も`exports`フィールドで提供
Node.jsとJavaScriptの世界は常に進化しており、ESMはその未来です。しかし、現実のプロジェクトではCommonJSとの共存も必要な場合が多いため、両方を理解しておくことが重要です。
適切なpackage.jsonとtsconfig.jsonの設定は、プロジェクトの安定性と将来性を大きく左右します。各フィールドの意味と影響を理解し、プロジェクトに最適な選択をしましょう!
開発が楽しくなるような設定構築ができれば幸いです。質問やコメントがあれば、ぜひ下のコメント欄でお聞かせください!