はじめに
TypeScript で Web 開発をしていると、モジュールシステムの設定で混乱することがありませんか?特に package.json の type フィールドと tsconfig.json の module オプション、どちらも "commonjs" など同じ値を設定できるので、「何が違うの?」と思った方もいるのではないでしょうか?
この記事では、実際にコードを書いて動かしながら、この2つの違いを整理してみます。
結論
-
package.json の
type: Node.js が.jsファイルをどう解釈するかを決める(実行に影響) -
tsconfig.json の
module: TypeScript コンパイラがどんな形式の JS コードを出力するかを決める(コンパイル時に影響)
開発環境
開発環境は以下の通りです。
- Windows11
- TypeScript 5.9.2
- Node.js 22.18.0
- npm 11.5.2
事前準備
まず、動作確認用にシンプルな TypeScript ファイルを作成します。
export const add = (a: number, b: number): number => {
return a + b;
};
export const multiply = (a: number, b: number): number => {
return a * b;
};
import { add, multiply } from './math';
console.log('2 + 3 =', add(2, 3));
console.log('4 * 5 =', multiply(4, 5));
パターン別検証
package.json の type と tsconfig の module の設定値ごとにコンパイルと実行結果を確認していきます。
パターン1: package.json type 未指定 + tsconfig module: "commonjs"
package.json と tsconfig をそれぞれ以下のように設定します。
{
"name": "module-test",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"devDependencies": {
"@types/node": "^24.3.1",
"typescript": "^5.9.02"
}
}
{
"compilerOptions": {
"target": "ES2024",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
}
}
index.ts のコンパイル結果は以下の通りです。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_1 = require("./math");
console.log("2 + 3 =", (0, math_1.add)(2, 3));
console.log("4 * 5 =", (0, math_1.multiply)(4, 5));
コンパイル結果からは、以下のことが読み取れます。
- TypeScript の
import/exportがrequire()/exportsに変換 -
"use strict"が追加され、厳格モードで実行 -
Object.defineProperty(exports, "__esModule", { value: true })でES Module との互換性を確保 -
package.jsonにtype指定がないため、Node.js は .js ファイルを CommonJS として認識
正常に実行できます。
> module-test@1.0.0 start
> node dist/index.js
2 + 3 = 5
4 * 5 = 20
tsconfig.json の module を "commonjs" にしたため、TypeScript が CommonJS 形式でコンパイルされました。また、package.json に type 指定がないため、Node.js は .js ファイルを CommonJS として認識しています。その結果、コンパイルされた .js ファイルと Node.js が認識する .js ファイルの形式が一致して正常に動作します。
パターン2: package.json type: "module" + tsconfig module: "commonjs"
package.json と tsconfig をそれぞれ以下のように設定します。
{
"name": "module-test",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
{
"compilerOptions": {
"target": "ES2024",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
}
}
ビルドして、実行する(npm run build && npm start)とエラーになります。
ReferenceError: require is not defined in ES module scope
TypeScript はパターン1と同じく require() を使った CommonJS 形式でコンパイルされました。しかし、package.json の "type": "module" により、Node.js は .js ファイルを ES Module として解釈しようとします。ES Module では require や exports は存在しないため、エラーになります。
package.json の type はコンパイル済みの .js ファイルに影響し、TypeScript のコンパイル自体には関与しません。
パターン3: package.json type: "module" + tsconfig module: "nodenext"
package.json と tsconfig をそれぞれ以下のように設定します。
{
"name": "module-test",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
{
"compilerOptions": {
"target": "ES2024",
"module": "nodenext",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
}
}
index.ts のコンパイル結果は以下の通りです。
import { add, multiply } from "./math.js";
console.log("2 + 3 =", add(2, 3));
console.log("4 * 5 =", multiply(4, 5));
コンパイル結果からは、以下のことが読み取れます。
- TypeScript が
import/export構文をそのまま残してコンパイル -
"use strict"やObject.definePropertyなどの CommonJS 特有の処理が存在しない
正常に実行できます。
> module-test@1.0.0 start
> node dist/index.js
2 + 3 = 5
4 * 5 = 20
まとめ
package.json の type と tsconfig.json の module は、それぞれ異なる役割を持っています:
- package.jsonのtype: Node.js ランタイムへの指示
- tsconfig.jsonのmodule: TypeScript コンパイラへの指示
この2つを適切に組み合わせることで、意図した通りのモジュールシステムでアプリケーションを動かすことができます。