はじめに
この記事では、TypeScriptにおける主要なモジュールシステムである CommonJS / ESM(ES Modules)について、実際のサンプルコードを交えながら比較して理解していきます。
各モジュールシステムの特徴、使い分け、そして実際の動作を検証することで、モジュールシステムの理解を深めていきます。
開発環境
開発環境は以下の通りです。
- Windows11
- Node.js 22.18.0
- npm 11.5.2
- TypeScript 5.9.2
- @types/node 24.3.0
モジュールシステムとは
モジュールシステムは、コードを複数のファイルに分割し、相互利用を可能にする仕組みです。TypeScript では、コンパイル時、指定したモジュールシステムに応じて JavaScript コードが生成されます。
主なモジュールシステムには、以下の3つがあります。
- CommonJS: Node.js で標準的に使用される同期的なモジュールシステム
- ESM: ECMAScript標準のモジュールシステム(ES2015で標準化)
1. CommonJS の実装と検証
1-1. プロジェクトのセットアップ
まず、CommonJSを検証するためのプロジェクトを作成します。
mkdir typescript-modules-commonjs
cd typescript-modules-commonjs
npm init -y
TypeScriptと必要なパッケージをインストールします。
npm install typescript @types/node --save-dev
npx tsc --init
1-2. CommonJS 用の tsconfig.json 設定
tsconfig.json を以下のように設定します。
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "CommonJS",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
主要な設定項目内容は、以下の通りです。
-
"module": "CommonJS"- TypeScript コンパイラに対して、CommonJS 形式でモジュールコードを生成するよう指示
-
import/export文がrequire()/exportsに変換される
-
"target": "ES2023"- 出力される JavaScript の ECMAScript バージョンを指定
-
ES2023を指定することで、モダンな JavaScript 機能を利用できる
-
"esModuleInterop": true- CommonJS モジュールを ES モジュール構文でインポートしやすくする
-
import * as fs from 'fs'ではなくimport fs from 'fs'のような書き方を可能にする
-
"outDir": "./dist"- コンパイル後の JavaScript ファイルの出力先ディレクトリを指定
1-3. CommonJSのサンプルコード実装
計算をする関数とそれを実行するコードを実装します。
// Named Export
export const add = (a: number, b: number): number => {
return a + b;
};
export const subtract = (a: number, b: number): number => {
return a - b;
};
// Default Export
const multiply = (a: number, b: number): number => {
return a * b;
};
export default multiply;
// オブジェクトとしてのExport
export const mathUtils = {
divide: (a: number, b: number): number => {
if (b === 0) throw new Error("Division by zero");
return a / b;
},
power: (base: number, exponent: number): number => {
return Math.pow(base, exponent);
},
};
// Named Import
import { add, subtract, mathUtils } from "./utils";
// Default Import
import multiply from "./utils";
// 全てをインポート
import * as Utils from "./utils";
console.log("=== CommonJS Module System ===");
console.log("add(10, 5):", add(10, 5));
console.log("subtract(10, 5):", subtract(10, 5));
console.log("multiply(10, 5):", multiply(10, 5));
console.log("mathUtils.divide(10, 5):", mathUtils.divide(10, 5));
console.log("mathUtils.power(2, 3):", mathUtils.power(2, 3));
console.log("\n=== Using namespace import ===");
console.log("Utils.add(20, 15):", Utils.add(20, 15));
console.log("Utils.default(20, 15):", Utils.default(20, 15));
1-4. 実行スクリプトの追加とビルド
package.json にスクリプトを追加します。
{
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "npm run build && npm start"
}
}
1-5. CommonJS の動作確認
先ほど実装したスクリプトを実行します。
npm run dev
実行結果は以下の通りです。
> typescript-modules-commonjs@1.0.0 dev
> npm run build && npm start
> typescript-modules-commonjs@1.0.0 build
> tsc
> typescript-modules-commonjs@1.0.0 start
> node dist/main.js
=== CommonJS Module System ===
add(10, 5): 15
subtract(10, 5): 5
multiply(10, 5): 50
mathUtils.divide(10, 5): 2
mathUtils.power(2, 3): 8
=== Using namespace import ===
Utils.add(20, 15): 35
Utils.default(20, 15): 300
生成されたJavaScriptファイル(dist/utils.js / dist/main.js)を確認してみます。
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mathUtils = exports.subtract = exports.add = void 0;
// Named Export
const add = (a, b) => {
return a + b;
};
exports.add = add;
const subtract = (a, b) => {
return a - b;
};
exports.subtract = subtract;
// Default Export
const multiply = (a, b) => {
return a * b;
};
exports.default = multiply;
// オブジェクトとしてのExport
exports.mathUtils = {
divide: (a, b) => {
if (b === 0)
throw new Error("Division by zero");
return a / b;
},
power: (base, exponent) => {
return Math.pow(base, exponent);
},
};
export は exports.xxx = xxx に変換されます。
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// Named Import
const utils_1 = require("./utils");
// Default Import
const utils_2 = __importDefault(require("./utils"));
// 全てをインポート
const Utils = __importStar(require("./utils"));
console.log("=== CommonJS Module System ===");
console.log("add(10, 5):", (0, utils_1.add)(10, 5));
console.log("subtract(10, 5):", (0, utils_1.subtract)(10, 5));
console.log("multiply(10, 5):", (0, utils_2.default)(10, 5));
console.log("mathUtils.divide(10, 5):", utils_1.mathUtils.divide(10, 5));
console.log("mathUtils.power(2, 3):", utils_1.mathUtils.power(2, 3));
console.log("\n=== Using namespace import ===");
console.log("Utils.add(20, 15):", Utils.add(20, 15));
console.log("Utils.default(20, 15):", Utils.default(20, 15));
import は require() に変換されます。
2. ESM(ES Modules)の実装と検証
2-1. ESMプロジェクトのセットアップ
mkdir typescript-modules-esm
cd typescript-modules-esm
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init
2-2. ESM用のpackage.json設定
package.json に "type": "module" を追加します。
この設定により、Node.js に対してこのパッケージが ESModules を使用することを明示します。未設定の場合、Node.jsは CommonJS として .js ファイルを解釈します。
{
"name": "typescript-modules-esm",
"version": "1.0.0",
"type": "module",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "npm run build && npm start"
}
}
2-3. ESM 用の tsconfig.json 設定
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023"],
"module": "ES2022",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}
主要な設定項目内容は、以下の通りです。
-
"module": "ES2023"- ES2023 形式でモジュールコードを生成
- ネイティブの
import/export構文が保持される
-
"moduleResolution": "node"- モジュール解決方法を Node.js スタイルに設定
-
node_modulesからのパッケージ解決や相対パス解決が適切に動作する
-
"allowSyntheticDefaultImports": true- デフォルトエクスポートがないモジュールに対してもデフォルトインポート構文を使えるようにする
-
"esModuleInterop": true- CommonJS モジュールと ESModule の相互運用性を向上
- CommonJS のモジュールを ESModule 構文でインポートしやすくする
2-4. ESMのサンプルコード実装
src/api.ts
// 型定義
export interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
// ユーザーデータのモック
const mockUsers: User[] = [
{ id: 1, name: "田中太郎", email: "tanaka@example.com", isActive: true },
{ id: 2, name: "佐藤花子", email: "sato@example.com", isActive: false },
{ id: 3, name: "鈴木次郎", email: "suzuki@example.com", isActive: true },
];
// API関数
export async function fetchUsers(): Promise<ApiResponse<User[]>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
data: mockUsers,
});
}, 1000);
});
}
export async function fetchUserById(id: number): Promise<ApiResponse<User>> {
return new Promise((resolve) => {
setTimeout(() => {
const user = mockUsers.find((u) => u.id === id);
if (user) {
resolve({
success: true,
data: user,
});
} else {
resolve({
success: false,
error: `User with id ${id} not found`,
});
}
}, 500);
});
}
// Default Export
export default class UserService {
async getActiveUsers(): Promise<User[]> {
const response = await fetchUsers();
if (response.success && response.data) {
return response.data.filter((user) => user.isActive);
}
return [];
}
async getUserProfile(id: number): Promise<User | null> {
const response = await fetchUserById(id);
return response.success && response.data ? response.data : null;
}
}
src/utils.ts
// 再エクスポート
export { User, ApiResponse } from './api.js';
// 独自のユーティリティ関数
export const formatUser = (user: User): string => {
const status = user.isActive ? '✅ アクティブ' : '❌ 非アクティブ';
return `${user.name} (${user.email}) - ${status}`;
};
export const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// 動的インポート用の関数
export const loadApiModule = async () => {
const apiModule = await import('./api.js');
return apiModule;
};
src/main.ts
// 様々なインポート方法の例
import UserService, { fetchUsers, fetchUserById } from './api.js';
import { formatUser, validateEmail, loadApiModule } from './utils.js';
import type { User, ApiResponse } from './api.js';
async function demonstrateESM(): Promise<void> {
console.log('=== ES Modules Demo ===\n');
// サービスクラスの使用
const userService = new UserService();
console.log('📊 アクティブユーザー取得中...');
const activeUsers = await userService.getActiveUsers();
activeUsers.forEach(user => {
console.log(` ${formatUser(user)}`);
});
console.log('\n👤 特定ユーザー情報取得中...');
const userProfile = await userService.getUserProfile(2);
if (userProfile) {
console.log(` ${formatUser(userProfile)}`);
}
console.log('\n📧 メールアドレス検証テスト:');
const testEmails = ['valid@example.com', 'invalid-email', 'test@domain.co.jp'];
testEmails.forEach(email => {
const isValid = validateEmail(email);
console.log(` ${email}: ${isValid ? '✅ 有効' : '❌ 無効'}`);
});
console.log('\n🔄 動的インポートのテスト:');
const apiModule = await loadApiModule();
const response: ApiResponse<User[]> = await apiModule.fetchUsers();
if (response.success && response.data) {
console.log(` 動的インポートで ${response.data.length} 人のユーザーを取得`);
}
console.log('\n✅ ES Modules Demo 完了');
}
// トップレベルawaitの使用(ESMの特徴)
await demonstrateESM();
- インポート時に拡張子(.js)を明示する必要がある
-
type: "module"を設定したESM環境では、TypeScriptファイル(.ts)のインポート先も.jsと記述する
2-5. ESMの動作確認
npm run dev
生成されたJavaScriptファイル(dist/api.js)を確認します。
// ユーザーデータのモック
const mockUsers = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com', isActive: true },
{ id: 2, name: '佐藤花子', email: 'sato@example.com', isActive: false },
{ id: 3, name: '鈴木次郎', email: 'suzuki@example.com', isActive: true }
];
// API関数
export async function fetchUsers() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
data: mockUsers
});
}, 1000);
});
}
export async function fetchUserById(id) {
return new Promise((resolve) => {
setTimeout(() => {
const user = mockUsers.find(u => u.id === id);
if (user) {
resolve({
success: true,
data: user
});
} else {
resolve({
success: false,
error: `User with id ${id} not found`
});
}
}, 500);
});
}
// Default Export
export default class UserService {
async getActiveUsers() {
const response = await fetchUsers();
if (response.success && response.data) {
return response.data.filter(user => user.isActive);
}
return [];
}
async getUserProfile(id) {
const response = await fetchUserById(id);
return response.success && response.data ? response.data : null;
}
}
- ネイティブの
import/export構文が保持される - トップレベル
awaitが使用できる - 静的解析によるツリーシェイキングが可能
まとめ
この記事では、TypeScriptにおける3つの主要なモジュールシステム(CommonJS、ESM)について、実際のサンプルコードを用いて比較検証しました。
各モジュールシステムには以下のような特徴があります。
- CommonJS: Node.js 環境での標準的な選択肢
- ESM: ECMAScript 標準でモダンな開発の主流
| 特徴 | CommonJS | ESM |
|---|---|---|
| 出力形式 | exports.xxx = xxx |
export xxx |
| 読み込み | require() |
import / await import()
|
| 実行環境 | Node.js中心 | モダンブラウザ・Node.js |
| 非同期対応 | 同期的 | 同期・非同期両対応 |