0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】CommonJS / ESM 比較して理解するモジュールシステム

Posted at

はじめに

この記事では、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 を以下のように設定します。

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のサンプルコード実装

計算をする関数とそれを実行するコードを実装します。

src/utils.ts
// 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);
  },
};
src/main.ts
// 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 にスクリプトを追加します。

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)を確認してみます。

dist/utils.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);
    },
};

exportexports.xxx = xxx に変換されます。

dist/main.js
"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));

importrequire() に変換されます。

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 ファイルを解釈します。

package.json
{
  "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 設定

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

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

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
非同期対応 同期的 同期・非同期両対応

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?