TypeScriptでCommonJSのrequireをESModulesのimportに移行するベストプラクティス

TypeScriptを利用している開発者の多くが、CommonJSのrequireを使ってモジュールをインポートしているかもしれません。しかし、JavaScriptの公式モジュールシステムであるESModulesが標準となり、特にTypeScriptプロジェクトでは、これに移行することが推奨されています。ESModulesは、よりシンプルで強力なモジュール管理を提供し、ツリーシェイキングや非同期ロードといったモダンな機能を活用できるためです。

本記事では、CommonJSからESModulesへの移行がなぜ重要なのかを確認し、移行を円滑に進めるためのベストプラクティスを解説します。モジュールの違いや設定変更、よくある問題点への対処方法まで、具体的な事例をもとに丁寧に説明していきます。

目次

CommonJSとESModulesの違い

CommonJSとは

CommonJSは、Node.jsで広く使われてきたモジュールシステムで、requiremodule.exportsを使用してモジュールのインポートやエクスポートを行います。サーバーサイドのJavaScript開発において標準的に採用されており、シンプルで即座に使える点が特徴です。しかし、クライアントサイドでの使用には制限があり、最新のJavaScript開発ではその柔軟性に欠ける場面が増えてきています。

ESModulesとは

一方、ESModules(ESM)は、JavaScriptの公式モジュールシステムとして、ECMAScript 2015 (ES6)で導入されました。ESModulesは、importexportを用いてモジュールを扱い、クライアントサイドおよびサーバーサイドの両方で広くサポートされています。非同期でのモジュール読み込みが可能で、ツリーシェイキングと呼ばれる不要なコードの除去もサポートしており、パフォーマンス面で優れています。

主要な違い

  1. 構文の違い: CommonJSはrequireでモジュールをインポートし、module.exportsでエクスポートします。一方、ESModulesではimportexportを使用します。
  • CommonJS: const module = require('module');
  • ESModules: import module from 'module';
  1. モジュールの読み込みタイミング: CommonJSは同期的にモジュールを読み込みますが、ESModulesはデフォルトで非同期的に読み込まれ、効率的なパフォーマンスを提供します。
  2. スコープの違い: CommonJSではモジュールごとに独立したスコープが作られますが、ESModulesはより厳密なスコープ管理を行い、変数の重複や予期せぬ動作を避けやすくします。
  3. 互換性: ESModulesはブラウザでも動作するため、フロントエンドとバックエンドの両方で同じモジュールシステムを使用できますが、CommonJSは基本的にNode.js環境に限定されます。

これらの違いにより、特にブラウザを含めたモダンな環境では、ESModulesへの移行が推奨されています。

ESModules移行のための準備

コードベースの整理

まず、CommonJSからESModulesへの移行を始める前に、既存のコードベースを整理する必要があります。これは、特に大規模なプロジェクトで、モジュールの依存関係や動的なrequire呼び出しが多い場合に重要です。コードベース全体でどのファイルがモジュールとして機能しているか、またどのファイルがrequireで依存しているかを把握することが移行の第一歩です。

依存ライブラリの確認

次に、プロジェクトで使用している外部ライブラリがESModulesに対応しているか確認します。多くのモダンなライブラリはすでにESModulesに対応していますが、古いライブラリの中にはCommonJSにしか対応していないものもあります。これらの互換性を確認し、必要であれば代替ライブラリへの移行を検討することが重要です。

TypeScriptコンパイラの設定変更

TypeScriptでESModulesを使用するには、tsconfig.jsonファイルでmoduleオプションをesnextまたはes2015に設定します。これにより、TypeScriptは出力コードとしてESModules形式を使用します。

{
  "compilerOptions": {
    "module": "esnext",
    "target": "es2015"
  }
}

この設定が完了していない場合、ESModulesに書き換えた後でも、正しくコンパイルされない可能性があるため、最初に行うべき重要なステップです。

モジュール境界の明確化

CommonJSではモジュール境界が曖昧な場合がありますが、ESModulesでは厳密なスコープ管理が求められます。すべてのファイルで、依存関係が明確に定義されているか確認しましょう。特に、グローバルスコープで定義された変数や関数が他のモジュールで使われている場合、それをimport/exportで明示的に管理する必要があります。

これらの準備作業を行うことで、ESModulesへのスムーズな移行が可能になります。

`require`から`import`への基本的な書き換え方法

単一のモジュールの書き換え

CommonJSのrequire文をESModulesのimport文に置き換える基本的な方法を紹介します。最もシンプルなケースでは、以下のように書き換えることができます。

  • CommonJS:
  const fs = require('fs');
  • ESModules:
  import fs from 'fs';

CommonJSでは、require関数を使ってモジュールを読み込んでいましたが、ESModulesではimport文を使ってモジュールを静的にインポートします。

オブジェクトとしてのエクスポートの書き換え

次に、オブジェクトや特定の関数をエクスポートしている場合の書き換えです。CommonJSではmodule.exportsを使いますが、ESModulesではexport defaultexportを使います。

  • CommonJS:
  const utils = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
  module.exports = utils;
  • ESModules:
  const utils = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
  export default utils;

module.exportsexport defaultに置き換えることで、デフォルトエクスポートとして同様の機能を提供します。

個別の名前付きエクスポートの書き換え

CommonJSではオブジェクト全体をエクスポートしますが、ESModulesでは名前付きエクスポートが可能です。これにより、特定の関数や変数のみをエクスポートすることができます。

  • CommonJS:
  exports.add = (a, b) => a + b;
  exports.subtract = (a, b) => a - b;
  • ESModules:
  export const add = (a, b) => a + b;
  export const subtract = (a, b) => a - b;

名前付きエクスポートにすることで、必要な関数だけをインポートでき、コードの明確化やパフォーマンス向上に寄与します。

インポートのエイリアス

CommonJSではモジュール全体をインポートする場合が多いですが、ESModulesでは個別のインポートに加えてエイリアスを使うこともできます。

  • CommonJS:
  const { add: sum } = require('./utils');
  • ESModules:
  import { add as sum } from './utils';

エイリアスを使うことで、特定の関数名をプロジェクトに合わせてカスタマイズしながら利用することができます。

このように、基本的な書き換えパターンを理解することで、プロジェクト全体のモジュール構造をスムーズにESModulesに移行することが可能です。

動的インポートの扱い

CommonJSでの動的インポート

CommonJSでは、requireを使用してモジュールを動的にインポートすることが簡単にできます。たとえば、条件に応じて異なるモジュールを読み込む場合、以下のようなコードが一般的です。

  • CommonJS:
  if (condition) {
    const moduleA = require('./moduleA');
  } else {
    const moduleB = require('./moduleB');
  }

requireは同期的に実行されるため、動的インポートでも即座にモジュールがロードされます。しかし、ESModulesでは同期的なimportがサポートされていないため、このアプローチを直接置き換えることはできません。

ESModulesでの動的インポート

ESModulesでは、動的インポートを行うために、import()という非同期関数を使用します。これはPromiseを返すため、非同期処理としてモジュールを動的にロードすることが可能です。上記のCommonJSの例を、ESModulesでは次のように書き換えます。

  • ESModules:
  if (condition) {
    import('./moduleA').then(moduleA => {
      // moduleAを使用
    });
  } else {
    import('./moduleB').then(moduleB => {
      // moduleBを使用
    });
  }

import()関数はPromiseを返すため、モジュールが読み込まれた後に次の処理を実行できます。この非同期インポートは、ブラウザやクライアントサイドでの使用において、パフォーマンスを向上させることができ、必要なタイミングでのみモジュールをロードすることで効率的なリソース管理が可能です。

動的インポートの利点

ESModulesの動的インポートには以下の利点があります。

  1. パフォーマンスの向上: 不要なモジュールを即時に読み込むことを避け、必要なときにのみモジュールをロードできるため、アプリケーションの初期ロード時間を短縮できます。
  2. 非同期処理との統合: 動的インポートはPromiseベースのため、async/awaitと組み合わせて使うことで、より直感的な非同期コードを書くことが可能です。
  • 例 (async/awaitでの使用):
  async function loadModule() {
    if (condition) {
      const moduleA = await import('./moduleA');
      // moduleAを使用
    } else {
      const moduleB = await import('./moduleB');
      // moduleBを使用
    }
  }

動的インポートの使用ケース

動的インポートは、以下のような場面で特に有効です。

  • ルートベースのコード分割: 大規模なフロントエンドアプリケーションでは、ページごとに異なるモジュールを動的にインポートすることで、最初に読み込むコード量を減らすことができます。
  • 条件に基づく依存関係の読み込み: 特定の条件下でしか必要としない依存ライブラリや、ユーザー操作に応じたモジュールの遅延ロードに適しています。

動的インポートを活用することで、アプリケーションのパフォーマンスやメンテナンス性が大きく向上します。

TypeScriptの`module`オプションの設定

TypeScriptコンパイラのモジュール設定

TypeScriptでは、コンパイル時に生成されるJavaScriptのモジュールシステムをtsconfig.jsonファイル内で指定することができます。moduleオプションは、この設定を決定する重要なパラメータです。CommonJSからESModulesに移行する際は、moduleオプションを適切に設定する必要があります。

設定の変更方法

TypeScriptのプロジェクトでCommonJSからESModulesに移行するためには、tsconfig.jsoncompilerOptionsmoduleオプションを次のように設定します。

  • CommonJSの場合:
  {
    "compilerOptions": {
      "module": "commonjs",
      "target": "es5"
    }
  }
  • ESModulesに変更する場合:
  {
    "compilerOptions": {
      "module": "esnext",
      "target": "es2015"
    }
  }

この設定により、TypeScriptコンパイラはESModules形式でコードを出力するようになります。module: "esnext"を指定することで、最新のESModulesを利用することが可能になります。また、targetes2015(またはそれ以降)に設定することで、最新のJavaScriptの機能を活用した出力が得られます。

具体的なコンパイラオプションの説明

  • module: このオプションは、TypeScriptがどのモジュールシステムを使用してJavaScriptコードを生成するかを指定します。esnextes2015を設定すると、ESModules形式で出力されます。
  • target: このオプションは、生成されるJavaScriptコードが対応するJavaScriptのバージョンを指定します。es2015以降を指定することで、ESModulesが利用可能となります。

その他の関連設定

ESModulesへの移行にあたり、moduleResolutionオプションも確認しておく必要があります。このオプションは、モジュールの解決方法を決定するもので、特にNode.jsのモジュール解決に関係しています。moduleResolutionは通常nodeに設定されますが、モジュールをブラウザ環境で使用する場合はclassicに設定することも可能です。

  • :
  {
    "compilerOptions": {
      "moduleResolution": "node"
    }
  }

移行時の注意点

TypeScriptプロジェクトをESModulesに移行する際、tsconfig.jsonを適切に設定することが重要です。特に、依存ライブラリやビルドプロセスがCommonJSに依存している場合、これらが正しくESModulesに対応しているか確認する必要があります。もし依存しているライブラリがCommonJSのみ対応の場合、ESModulesと共存するための設定や修正が必要になることがあります。

このmoduleオプションの設定により、TypeScriptプロジェクト全体が最新のESModulesへと対応するようになり、パフォーマンス向上やモダンなJavaScriptの機能を活用できるようになります。

CommonJSモジュールとの互換性保持方法

ESModulesとCommonJSの共存

TypeScriptプロジェクトでESModulesに移行する際、多くの場合、既存のCommonJSモジュールと共存する必要があります。特に外部ライブラリや依存関係がCommonJSで書かれている場合、完全に移行するまでの過渡期に両方のモジュールシステムが同時に存在することになります。そこで、互換性を維持しながらESModulesに移行する方法を見ていきます。

CommonJSモジュールをESModulesでインポートする

ESModulesからCommonJSモジュールをインポートする場合、通常のimport文でそのままインポートできますが、CommonJSモジュールのエクスポート方法に応じて注意が必要です。module.exportsを使ったモジュールは、デフォルトエクスポートとして扱われます。

  • CommonJSモジュールの例:
  // utils.js (CommonJS)
  module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
  • ESModulesでのインポート:
  import utils from './utils';
  console.log(utils.add(2, 3)); // 5

このように、CommonJSモジュールはESModulesのデフォルトエクスポートとして扱われるため、そのままimport文で読み込むことができます。

CommonJSの名前付きエクスポートの扱い

CommonJSではexportsを使って名前付きで複数の関数や変数をエクスポートすることがあります。ESModulesでは、これをそのまま利用するために少し異なる構文を使うことが必要です。

  • CommonJSの名前付きエクスポート例:
  // math.js (CommonJS)
  exports.add = (a, b) => a + b;
  exports.subtract = (a, b) => a - b;
  • ESModulesでのインポート:
  import { add, subtract } from './math';
  console.log(add(2, 3)); // 5
  console.log(subtract(5, 2)); // 3

名前付きエクスポートはそのまま名前を指定してimportできるため、互換性を維持しつつ使用することが可能です。

ESModulesをCommonJSで利用する

逆に、CommonJS形式のプロジェクトでESModulesを使用する場合には、ESModulesのエクスポートが自動的にrequireで読み込めるようにはなりません。import文でエクスポートしたESModulesをrequireで利用するには、次のような変換が必要です。

  • ESModulesの例:
  // calculator.mjs (ESModules)
  export const add = (a, b) => a + b;
  export const subtract = (a, b) => a - b;
  • CommonJSでのインポート:
  const calculator = await import('./calculator.mjs');
  console.log(calculator.add(2, 3)); // 5

ここで重要なのは、CommonJS側ではimport()関数を使って非同期でモジュールを読み込む必要がある点です。これはESModulesが非同期で読み込まれるためです。

TypeScriptでの互換性設定

TypeScriptでは、ESModulesとCommonJSの互換性を保つために、esModuleInteropオプションを使用します。このオプションを有効にすると、CommonJSモジュールをESModules形式で扱いやすくなります。

  • tsconfig.jsonの設定:
  {
    "compilerOptions": {
      "esModuleInterop": true
    }
  }

このオプションにより、CommonJSとESModulesの互換性が向上し、コードの書き換えがスムーズに進みます。

互換性を保ちながらの移行のポイント

  • 段階的な移行: 一度にすべてをESModulesに書き換えるのではなく、モジュール単位で徐々に移行することを推奨します。依存関係が多いプロジェクトでは、この方法が現実的です。
  • 互換ライブラリの利用: 一部のライブラリはCommonJSとESModulesの両方をサポートしています。これらのライブラリを活用することで、移行時のトラブルを軽減できます。

このように、CommonJSモジュールとの互換性を保持しながらESModulesへの移行を進めることは可能です。段階的に対応しつつ、ライブラリや設定を適切に利用することが重要です。

トラブルシューティング:エラーと対策

移行時に発生する一般的なエラー

CommonJSからESModulesに移行する際、いくつかのエラーや問題が発生することがあります。特に、requireimportの違いやモジュールの読み込みタイミングによるエラーがよく見られます。ここでは、よくあるエラーとその対策を見ていきます。

エラー 1: `require is not defined`

ESModulesに移行した際に、ブラウザやTypeScriptコンパイラから「require is not defined」というエラーが出ることがあります。これは、requireがCommonJS専用の関数であり、ESModulesでは使用できないためです。

  • 解決策:
    requireを使用していた部分をimportに書き換える必要があります。たとえば、以下のように修正します。
  • 修正前 (CommonJS): const fs = require('fs');
  • 修正後 (ESModules): import fs from 'fs'; requireを残してしまうと、このエラーが発生するため、すべてのモジュールインポートがimport文になっているか確認しましょう。

エラー 2: `Cannot use import statement outside a module`

ESModulesを使用する場合、TypeScriptやNode.jsの設定によっては、「Cannot use import statement outside a module」というエラーが出ることがあります。これは、ESModulesが適切に認識されていない場合に発生します。

  • 解決策:
    tsconfig.jsonmoduleオプションが適切に設定されているかを確認してください。ESModulesを利用する場合、moduleesnextes2015に設定する必要があります。また、Node.js環境で動作させる際は、.mjs拡張子を使用するか、package.json"type": "module"を指定します。
  • package.jsonの例:
    json { "type": "module" }

これにより、Node.jsがESModulesを認識できるようになります。

エラー 3: 動的インポートのエラー

CommonJSで使用していたrequireによる動的インポートをESModulesに書き換えるときに、非同期処理に関するエラーが発生することがあります。特に、同期的なrequireを非同期のimport()に書き換えた際、Promiseの扱いに関するエラーが発生することがあります。

  • 解決策:
    動的インポートはPromiseを返すため、async/awaitを使って適切に非同期処理を行う必要があります。
  • 修正前 (CommonJS): const moduleA = require('./moduleA');
  • 修正後 (ESModules): async function loadModule() { const moduleA = await import('./moduleA'); // モジュールの使用 } このように、非同期処理としてインポートを扱うことで、エラーを防ぐことができます。

エラー 4: `default`エクスポートに関する問題

CommonJSのモジュールをESModulesでインポートした際に、「module.default is undefined」というエラーが出る場合があります。これは、CommonJSモジュールをexport defaultのように扱おうとした際に発生します。

  • 解決策:
    esModuleInteropオプションを有効にして、CommonJSモジュールをESModulesと互換性のある形で扱うようにします。tsconfig.jsonに次の設定を追加してください。
  • tsconfig.jsonの設定: { "compilerOptions": { "esModuleInterop": true } } これにより、CommonJSのデフォルトエクスポートをimport文で正しくインポートできるようになります。

エラー 5: モジュール解決エラー

ESModulesでは、モジュールの解決がCommonJSと異なるため、モジュールのパスに関連するエラーが発生することがあります。特に、拡張子が自動的に補完されないため、import './module'のような形式でインポートした場合に「モジュールが見つからない」というエラーが出ることがあります。

  • 解決策:
    モジュールのインポート時に、必ず拡張子を含めて指定します。
  • 修正前: import moduleA from './moduleA';
  • 修正後: import moduleA from './moduleA.js'; ESModulesでは、JavaScriptファイルの拡張子が省略できないため、.js.tsを明示的に指定する必要があります。

デバッグ時のヒント

ESModulesへの移行中にエラーが発生した場合、以下のデバッグ手法が役立ちます。

  1. ログ出力: インポートやエクスポートの部分で適切にモジュールが読み込まれているか、console.log()を使って確認します。
  2. 型チェック: TypeScriptの型チェック機能を活用して、モジュール間の依存関係が正しく定義されているか確認します。型エラーが発生している場合、移行時に見落としている部分があるかもしれません。
  3. ビルドプロセスの確認: WebpackやRollupなどのモジュールバンドラを使用している場合、設定がESModulesに対応しているか確認します。特に、出力形式やモジュール解決に関する設定が重要です。

これらのエラーと対策を理解し、トラブルシューティングを行うことで、CommonJSからESModulesへの移行をスムーズに進めることができます。

モジュール移行のテスト手法

移行後のコード検証の重要性

CommonJSからESModulesへ移行した後、コードが正しく動作するかを確認するためのテストは非常に重要です。モジュールの動作が正しいか、依存関係が正しく解決されているか、パフォーマンスに問題がないかをチェックすることで、移行による不具合やパフォーマンス低下を防ぐことができます。ここでは、移行後のテスト手法をいくつか紹介します。

ユニットテストによるモジュール検証

まず、移行後のモジュールが単体で正しく動作しているかを確認するために、ユニットテストを行います。特に、importでモジュールを読み込んだ部分が正しく動作しているかをテストします。移行前に書かれたユニットテストがある場合、それを再利用して動作確認を行いましょう。

  • 例 (Jestを使用したテスト):
  import { add } from './math';

  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });

移行前後でテストがすべてパスすることを確認することで、モジュールの移行が正しく行われたかを確認できます。

インテグレーションテストによるモジュール間の動作確認

移行によってモジュール間の依存関係が壊れていないか確認するために、インテグレーションテストを実施します。複数のモジュールが連携して動作する場合、それらが正しくインポートされ、予期した通りに機能しているかを確認します。

  • :
    複数のモジュール間でデータの受け渡しや処理が行われているかをテストします。例えば、moduleAmoduleBに依存している場合、両方のモジュールをインポートして連携動作をテストします。
  import { calculate } from './calculator';
  import { add } from './math';

  test('calculates correct result with addition', () => {
    const result = calculate(1, 2, add);
    expect(result).toBe(3);
  });

このように、モジュール間の相互作用を確認することで、移行後に依存関係の問題がないか検証します。

End-to-End (E2E)テストでの動作確認

大規模なプロジェクトでは、ユーザーの視点からアプリケーション全体が正しく動作しているかを確認するために、E2Eテストを行います。ESModulesに移行した後、全体の機能が予想通りに動作しているかをシミュレーションし、テストします。これにより、個々のモジュールの動作だけでなく、システム全体の一貫性も確認できます。

  • 例 (Cypressを使用したE2Eテスト):
  describe('Application functionality', () => {
    it('should load the main page and display content', () => {
      cy.visit('/');
      cy.contains('Welcome to the App').should('be.visible');
    });
  });

E2Eテストを実施することで、モジュールの移行がアプリケーション全体に悪影響を及ぼしていないかを確認できます。

パフォーマンステスト

モジュールをESModulesに移行したことで、パフォーマンスが向上しているか、もしくは悪化していないかを確認するためのテストも重要です。特に、ESModulesの非同期インポートを利用することで、初期読み込み時間が改善されているか、モジュールの分割が適切に行われているかを確認します。

  • パフォーマンス測定ツール:
  • ブラウザの開発者ツールの「ネットワーク」タブを使用して、モジュールのロード時間を計測します。
  • WebpackやRollupなどのバンドラーを使用して、最終的なバンドルサイズが縮小されているか確認します。

TypeScriptの型チェック

TypeScriptを使用している場合、移行後に型チェックを行うことで、静的にコードの正確性を検証することができます。tscコマンドを実行して、型エラーが発生していないか確認します。モジュールのインポートやエクスポートに問題がある場合、型チェックによってすぐに検出されるため、エラーを防ぐことができます。

  • コマンド例:
  tsc --noEmit

このコマンドは、コンパイルせずに型チェックだけを行い、エラーがあるかどうかを確認します。

テスト自動化の活用

テストは自動化して継続的に実施できるように設定するのが理想的です。CI/CD(継続的インテグレーション/デリバリー)環境でテストを自動化し、ESModules移行後に新たな変更が問題を引き起こさないよう、継続的にテストを行います。

  • : GitHub ActionsやJenkinsなどを使って、コードがプッシュされた際に自動でテストが実行されるように設定します。

これらのテスト手法を組み合わせて実施することで、CommonJSからESModulesへの移行後に発生する可能性のある問題を未然に防ぎ、コードの品質を維持できます。

実践:移行の実例

プロジェクトでの移行手順の概要

CommonJSからESModulesへの移行を行う際、実際のプロジェクトでどのように進めるかを具体的な手順とともに解説します。ここでは、シンプルなTypeScriptプロジェクトを例に、ステップごとに移行プロセスを説明します。この手順を追うことで、移行の流れや各ポイントの注意点が把握できるでしょう。

ステップ1: 現状のプロジェクト確認

まず、移行を始める前に、現状のプロジェクト構成を把握します。すべてのモジュールがどのようにインポートされ、エクスポートされているか、また、依存関係がどのようになっているかを確認します。特にrequiremodule.exportsが使われている箇所をすべて洗い出すことが重要です。

  • プロジェクト構成例:
  ├── src
  │   ├── index.ts
  │   ├── math.ts
  │   └── utils.ts
  └── package.json

このプロジェクトは、index.tsmath.tsutils.tsrequireでインポートしています。また、他にも外部ライブラリがCommonJS形式でインポートされているかもしれません。

ステップ2: `tsconfig.json`の設定変更

次に、プロジェクトの設定ファイルであるtsconfig.jsonをESModulesに対応するように変更します。具体的には、moduleオプションをesnextに変更し、esModuleInteropオプションも有効にして、モジュールの互換性を確保します。

  • tsconfig.jsonの変更前:
  {
    "compilerOptions": {
      "module": "commonjs",
      "target": "es5"
    }
  }
  • tsconfig.jsonの変更後:
  {
    "compilerOptions": {
      "module": "esnext",
      "target": "es2015",
      "esModuleInterop": true
    }
  }

この変更により、TypeScriptがESModules形式でコードをコンパイルし、互換性のあるインポート処理が行われます。

ステップ3: `require`を`import`に書き換え

プロジェクト全体で、requireimportに置き換えます。ここでは、index.ts内のrequire文をimportに書き換えます。

  • 変更前 (index.ts):
  const math = require('./math');
  const utils = require('./utils');
  • 変更後 (index.ts):
  import * as math from './math';
  import * as utils from './utils';

さらに、module.exportsを使用していたファイルは、exportまたはexport defaultに変更します。

  • 変更前 (math.ts):
  module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
  • 変更後 (math.ts):
  export const add = (a, b) => a + b;
  export const subtract = (a, b) => a - b;

これにより、モジュールのエクスポートとインポートがESModules形式に変わり、最新のモジュール仕様に準拠したコードとなります。

ステップ4: 動的インポートの変更

もしプロジェクト内で動的なrequireを使用している場合、これを非同期のimport()に書き換えます。requireは同期的にモジュールを読み込むのに対し、ESModulesのimport()はPromiseを返し、非同期でモジュールを読み込みます。

  • 変更前 (動的require):
  const config = require(condition ? './configA' : './configB');
  • 変更後 (動的import):
  const config = await import(condition ? './configA' : './configB');

このように動的なインポートもESModulesに対応する形に変更します。

ステップ5: テストと動作確認

すべての書き換えが完了したら、ユニットテストやインテグレーションテストを実行して、プロジェクトが正しく動作するか確認します。特に、依存関係が正しくインポート・エクスポートされているか、動的インポートが期待通りに動作しているかを重点的にテストします。

  • テストの実行 (例: Jest):
  npm run test

移行前後でのテスト結果を比較し、すべてのテストが成功することを確認します。もしエラーが発生した場合、モジュールの依存関係やインポート方法に問題がある可能性があるため、コードを見直します。

ステップ6: デプロイとパフォーマンスチェック

テストが通ったら、移行後のコードを本番環境にデプロイし、パフォーマンスチェックを行います。ESModulesに移行することで、初期読み込み時間の短縮やモジュールの分割によるパフォーマンス向上が期待されます。ブラウザの開発者ツールやビルドツールを使用して、モジュールのロード時間やバンドルサイズが最適化されているか確認します。

  • 例: Webpackバンドルサイズの確認:
  npm run build

ビルド後に生成されたファイルのサイズが縮小され、ESModulesの効果が出ていることを確認します。

ステップ7: 継続的なモジュール最適化

ESModulesへの移行が完了した後も、定期的に依存関係やモジュールの使用状況を見直し、不要なモジュールを削除したり、パフォーマンスを向上させるための最適化を行います。また、新しいライブラリやモジュールを導入する際には、ESModulesに対応しているものを優先的に選択します。

このような移行手順に従って、プロジェクト全体を段階的にESModulesに対応させることができます。実際のプロジェクトでは、移行後のテストや最適化が特に重要です。これにより、最新のJavaScriptモジュール機能を最大限に活用できるようになります。

総合演習:プロジェクトの移行シミュレーション

演習の概要

ここでは、CommonJSからESModulesへの移行を実際にシミュレーションする演習を行います。この演習では、シンプルなTypeScriptプロジェクトを使い、requiremodule.exportsimportexportに書き換えながら、実際に移行作業を体験します。これにより、移行時に直面する可能性のある問題に対応するためのスキルを身につけることができます。

ステップ1: プロジェクトの初期セットアップ

まず、演習用のプロジェクトをセットアップします。以下のようなディレクトリ構成のプロジェクトを用意してください。

  • プロジェクト構成:
  ├── src
  │   ├── index.ts
  │   ├── math.ts
  │   └── utils.ts
  ├── package.json
  └── tsconfig.json
  • package.jsonの内容:
  {
    "name": "module-migration-example",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
      "build": "tsc",
      "start": "node dist/index.js"
    },
    "devDependencies": {
      "typescript": "^4.0.0"
    }
  }
  • tsconfig.jsonの内容:
  {
    "compilerOptions": {
      "target": "es5",
      "module": "commonjs",
      "outDir": "./dist",
      "rootDir": "./src"
    }
  }

このプロジェクトは、現在CommonJSを使用しており、requiremodule.exportsを使用してモジュール間の依存関係を管理しています。

ステップ2: `math.ts`ファイルの移行

次に、math.tsファイルのmodule.exportsexportに書き換えます。CommonJSからESModulesへの移行を体験するための基本的な手順です。

  • 変更前 (math.ts):
  module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
  };
  • 変更後 (math.ts):
  export const add = (a, b) => a + b;
  export const subtract = (a, b) => a - b;

このように、module.exportsをESModulesのexportに書き換え、モジュールのエクスポート方法をモダンな形式に変更します。

ステップ3: `index.ts`ファイルの移行

次に、index.ts内でrequireを使っていた部分を、importに書き換えます。

  • 変更前 (index.ts):
  const math = require('./math');
  console.log(math.add(2, 3));
  • 変更後 (index.ts):
  import { add } from './math';
  console.log(add(2, 3));

この変更により、import文を使ってモジュールをインポートするようになり、ESModulesに対応したコードとなります。

ステップ4: `tsconfig.json`の設定変更

次に、TypeScriptの設定ファイルtsconfig.jsonを変更して、プロジェクト全体がESModulesを使用するようにします。

  • 変更前 (tsconfig.json):
  {
    "compilerOptions": {
      "target": "es5",
      "module": "commonjs",
      "outDir": "./dist",
      "rootDir": "./src"
    }
  }
  • 変更後 (tsconfig.json):
  {
    "compilerOptions": {
      "target": "es2015",
      "module": "esnext",
      "esModuleInterop": true,
      "outDir": "./dist",
      "rootDir": "./src"
    }
  }

この変更により、TypeScriptはESModules形式でJavaScriptを出力するようになります。

ステップ5: 動的インポートの演習

次に、動的にモジュールを読み込むためのコードを書き換えます。requireを使っていた部分を、非同期のimport()関数を使うように変更します。

  • 変更前 (index.ts):
  if (condition) {
    const moduleA = require('./moduleA');
  }
  • 変更後 (index.ts):
  if (condition) {
    import('./moduleA').then(moduleA => {
      // moduleAを使用
    });
  }

これにより、動的インポートがPromiseベースで行われ、非同期処理と統合されます。

ステップ6: テストと確認

移行が完了したら、ユニットテストを実行して、移行が正しく行われたことを確認します。

  • テスト内容の例:
  import { add } from './math';

  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });

テストがすべて成功すれば、移行が正常に完了したことが確認できます。

ステップ7: デプロイと実運用環境での確認

最後に、移行後のプロジェクトを本番環境にデプロイし、すべてが期待通りに動作しているか確認します。ブラウザの開発者ツールやサーバーログを使って、パフォーマンスやエラーの有無を確認します。


この演習を通じて、CommonJSからESModulesへの移行手順を実際に体験できます。プロジェクト全体を段階的に移行し、モジュールの依存関係や動的インポートの処理方法を理解することが目的です。

まとめ

本記事では、CommonJSからESModulesへの移行に必要なステップと、それに伴うトラブルシューティング、テスト方法、実際の移行例を詳しく解説しました。ESModulesは、非同期インポートやツリーシェイキングなどのモダンな機能を提供し、パフォーマンスの向上やメンテナンス性の改善に貢献します。移行をスムーズに行うためには、プロジェクト全体の構造を整理し、段階的にモジュールの書き換えとテストを進めることが重要です。

ESModulesへの移行は、今後のJavaScript開発において不可欠な要素となるため、早めに対応することで、プロジェクトの長期的な発展に役立ちます。

コメント

コメントする

目次