はじめに
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つを適切に組み合わせることで、意図した通りのモジュールシステムでアプリケーションを動かすことができます。