T-CREATOR

ESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す

ESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す

TypeScript プロジェクトで突然「ERR_REQUIRE_ESM」エラーが出たり、import 文が使えなくなったりする経験、ありませんか? この問題は、JavaScript のモジュールシステムの過渡期に起こる「ESM(ECMAScript Modules)」と「CJS(CommonJS)」の互換性問題です。

本記事では、これらのエラーを根本から理解し、TypeScript プロジェクトで確実に解決する方法をご紹介します。

背景

JavaScript のモジュールシステムとは

JavaScript には、コードを分割して管理するための「モジュールシステム」が 2 種類存在します。 これらは異なる仕組みで動作するため、混在すると互換性の問題が発生するのです。

以下の図で、2 つのモジュールシステムの関係性を確認しましょう。

mermaidflowchart TB
  subgraph old["従来の仕組み(2009年〜)"]
    cjs["CommonJS (CJS)"]
    cjs_syntax["require() / module.exports"]
  end

  subgraph modern["現代の標準(2015年〜)"]
    esm["ECMAScript Modules (ESM)"]
    esm_syntax["import / export"]
  end

  cjs --> cjs_syntax
  esm --> esm_syntax

  old -.->|移行期| modern

  style old fill:#fff3cd
  style modern fill:#d1ecf1

この図が示すように、JavaScript のエコシステムは CJS から ESM へと移行している最中です。

CommonJS(CJS)の特徴

Node.js が誕生した当初から使われてきた、従来のモジュールシステムです。 同期的に動作し、シンプルで使いやすいのが特徴でした。

CJS のコード例

typescript// ファイルのインポート
const express = require('express');
const { readFile } = require('fs');
typescript// ファイルのエクスポート
module.exports = {
  apiVersion: '1.0.0',
  handler: function () {
    // 処理
  },
};

ECMAScript Modules(ESM)の特徴

ES6(ES2015)で JavaScript の標準仕様に追加された、新しいモジュールシステムです。 非同期処理に対応し、静的解析が可能で、Tree Shaking(未使用コードの削除)などの最適化ができます。

ESM のコード例

typescript// ファイルのインポート
import express from 'express';
import { readFile } from 'fs';
typescript// ファイルのエクスポート
export const apiVersion = '1.0.0';

export function handler() {
  // 処理
}

2 つのシステムが共存する現状

現在の JavaScript エコシステムでは、古い CJS パッケージと新しい ESM パッケージが混在しています。 npm に公開されているパッケージの中には、ESM のみをサポートするものが増えてきており、これが互換性問題の原因となっているのです。

#項目CommonJS (CJS)ECMAScript Modules (ESM)
1構文require() / module.exportsimport / export
2読み込み同期非同期
3ファイル拡張子.js / .cjs.mjs / .js (package.json で指定)
4Node.js 対応初期から対応Node.js 12 以降
5Tree Shaking不可可能
6ブラウザ対応不可(bundler 必須)ネイティブ対応

課題

よく遭遇するエラーパターン

TypeScript プロジェクトで ESM/CJS の問題に直面すると、以下のようなエラーに遭遇します。 それぞれのエラーには明確な原因があり、適切な対処法が存在するのです。

パターン 1:ERR_REQUIRE_ESM エラー

エラーコード: ERR_REQUIRE_ESM

bashError [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/package/index.js not supported.
Instead change the require of index.js to a dynamic import() which is available in all CommonJS modules.

発生条件:

  • CJS モードで動作しているコードから、ESM 専用パッケージを require() で読み込もうとした場合
  • 主に chalk v5 以降、node-fetch v3 以降、execa v6 以降などの ESM 専用パッケージで発生

パターン 2:import 文が使えない

エラーコード: SyntaxError

bashSyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:360:18)
    at wrapSafe (node:internal/modules/cjs/loader:1088:15)

発生条件:

  • package.json"type": "module" の指定がない状態で、.js ファイルに import 文を記述した場合
  • Node.js が該当ファイルを CJS として解釈しようとして失敗

パターン 3:TypeScript と Node.js の設定の不一致

エラーコード: TypeError

bashTypeError: Cannot read property 'default' of undefined

発生条件:

  • TypeScript の module 設定と package.jsontype フィールドが一致していない場合
  • トランスパイル後のコードが想定と異なるモジュール形式で出力される

以下の図で、エラーが発生する典型的なフローを確認しましょう。

mermaidflowchart TD
  start["TypeScript コードを実行"] --> check_pkg["package.json を確認"]
  check_pkg --> has_type{type フィールド<br/>の指定は?}

  has_type -->|"指定なし(CJS)"| cjs_mode["CJS モードで実行"]
  has_type -->|"module"| esm_mode["ESM モードで実行"]

  cjs_mode --> check_import_cjs{"import 文を<br/>使用している?"}
  check_import_cjs -->|はい| err1["❌ SyntaxError<br/>import が使えない"]
  check_import_cjs -->|いいえ| check_esm_pkg{"ESM 専用<br/>パッケージを<br/>require() している?"}

  check_esm_pkg -->|はい| err2["❌ ERR_REQUIRE_ESM"]
  check_esm_pkg -->|いいえ| success1["✓ 正常動作"]

  esm_mode --> check_require{"require() を<br/>使用している?"}
  check_require -->|はい| err3["❌ ReferenceError<br/>require is not defined"]
  check_require -->|いいえ| success2["✓ 正常動作"]

  style err1 fill:#f8d7da
  style err2 fill:#f8d7da
  style err3 fill:#f8d7da
  style success1 fill:#d4edda
  style success2 fill:#d4edda

図で理解できる要点:

  • Node.js は package.jsontype フィールドでモジュールシステムを判断する
  • CJS モードでは import 文や ESM パッケージが使えない
  • ESM モードでは require() が使えない

なぜこの問題が起こるのか

この問題の根本原因は、JavaScript エコシステムの「移行期」にあります。 npm パッケージの作者が ESM への移行を進める一方で、既存のプロジェクトは CJS のままというケースが多いのです。

特に以下のパッケージは ESM 専用となり、多くの開発者を悩ませています。

#パッケージ名ESM 専用になったバージョン用途
1chalkv5.0.0 以降ターミナル文字の色付け
2node-fetchv3.0.0 以降HTTP リクエスト
3execav6.0.0 以降プロセス実行
4gotv12.0.0 以降HTTP クライアント
5p-queuev7.0.0 以降Promise キュー

解決策

解決方針の選び方

ESM/CJS 問題を解決するには、プロジェクトの状況に応じて適切な方針を選ぶ必要があります。 以下のフローチャートで、あなたのプロジェクトに最適な解決策を見つけましょう。

mermaidflowchart TD
  start["ESM/CJS エラーが発生"] --> question1{"新規プロジェクト<br/>または全面刷新可能?"}

  question1 -->|はい| solution1["✓ 解決策 A<br/>完全に ESM へ移行"]
  question1 -->|いいえ| question2{"ESM 専用<br/>パッケージを<br/>使う必要がある?"}

  question2 -->|はい| question3{"そのパッケージの<br/>旧バージョン(CJS)で<br/>要件を満たせる?"}
  question2 -->|いいえ| solution2["✓ 解決策 B<br/>CJS のまま維持"]

  question3 -->|はい| solution3["✓ 解決策 C<br/>旧バージョンを使用"]
  question3 -->|いいえ| question4{"dynamic import()<br/>に書き換え可能?"}

  question4 -->|はい| solution4["✓ 解決策 D<br/>dynamic import() で対応"]
  question4 -->|いいえ| solution1

  style solution1 fill:#d1ecf1
  style solution2 fill:#d1ecf1
  style solution3 fill:#d1ecf1
  style solution4 fill:#d1ecf1

図で理解できる要点:

  • 新規プロジェクトなら ESM への完全移行がおすすめ
  • 既存プロジェクトでは、要件に応じて CJS 維持や dynamic import() を検討
  • パッケージの旧バージョン利用も有効な選択肢

それでは、各解決策を詳しく見ていきましょう。

解決策 A:完全に ESM へ移行する(推奨)

最も根本的な解決方法は、プロジェクト全体を ESM に移行することです。 これにより、最新のパッケージを使え、将来的な互換性問題も回避できます。

ステップ 1:package.json に type フィールドを追加

プロジェクトが ESM を使用することを Node.js に伝えます。

json{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module"
}

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

TypeScript が ESM 形式でコードを出力するように設定します。

json{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2020",
    "moduleResolution": "node"
  }
}

各設定項目の意味は以下の通りです。

#設定項目推奨値説明
1moduleESNextESM 形式でコードを出力
2targetES2020 以降async/await などの新機能を使用可能に
3moduleResolutionnode または bundlerモジュール解決方法を指定
4esModuleInteroptrueCJS との相互運用性を向上
5allowSyntheticDefaultImportstruedefault import の柔軟性を向上

ステップ 3:すべての import/export 文を ESM に統一

既存のコードを ESM の構文に書き換えます。

変更前(CJS):

typescript// ❌ CommonJS の書き方
const express = require('express');
const { readFile } = require('fs').promises;

module.exports = {
  startServer,
};

変更後(ESM):

typescript// ✓ ESM の書き方
import express from 'express';
import { readFile } from 'fs/promises';

export { startServer };

ステップ 4:ファイル拡張子を明示的に指定

ESM では、import 文でファイル拡張子を省略できません。 ローカルモジュールをインポートする際は、必ず .js 拡張子を付けてください。

変更前:

typescript// ❌ 拡張子がない
import { config } from './config';
import { Logger } from './utils/logger';

変更後:

typescript// ✓ .js 拡張子を明示
import { config } from './config.js';
import { Logger } from './utils/logger.js';

重要な注意点: TypeScript のソースファイルは .ts ですが、インポート時は .js を指定します。 これは、トランスパイル後のファイル(.js)を参照するためです。

ステップ 5:**dirname と **filename の代替実装

CJS で使えた __dirname__filename は、ESM では使用できません。 以下のように書き換える必要があります。

変更前(CJS):

typescript// ❌ CJS で使える変数
const currentDir = __dirname;
const currentFile = __filename;

変更後(ESM):

typescript// ✓ ESM での代替実装
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

import.meta.url は、現在のモジュールの URL を取得する ESM の標準機能です。 これを fileURLToPath() で変換することで、ファイルパスを取得できます。

解決策 B:CJS のまま維持する

既存の大規模プロジェクトで、ESM への移行コストが高い場合は、CJS を維持する選択肢もあります。 ただし、ESM 専用パッケージは使用できないため、注意が必要です。

package.json の設定確認

type フィールドを指定しないか、明示的に commonjs を指定します。

json{
  "name": "my-project",
  "version": "1.0.0",
  "type": "commonjs"
}

tsconfig.json の設定

CJS 形式でコードを出力するように設定します。

json{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2019",
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}

CJS で統一したコード例

typescript// require() でインポート
const express = require('express');
const { readFileSync } = require('fs');
typescript// module.exports でエクスポート
function createServer() {
  // 処理
}

module.exports = {
  createServer,
};

解決策 C:ESM 専用パッケージの旧バージョンを使用

ESM 専用パッケージの最新版が使えない場合、CJS をサポートする旧バージョンに固定する方法もあります。

chalk の例

chalk v5 以降は ESM 専用ですが、v4 までは CJS をサポートしています。

json{
  "dependencies": {
    "chalk": "^4.1.2"
  }
}

旧バージョン使用時のコード例

typescript// chalk v4(CJS 対応版)
const chalk = require('chalk');

console.log(chalk.blue('Hello World'));
console.log(chalk.red.bold('Error!'));

主要パッケージの CJS 対応最終バージョン

#パッケージ名CJS 対応最終版ESM 専用初版備考
1chalkv4.1.2v5.0.0ターミナル色付け
2node-fetchv2.7.0v3.0.0HTTP クライアント
3execav5.1.1v6.0.0プロセス実行
4gotv11.8.6v12.0.0HTTP リクエスト
5p-queuev6.6.2v7.0.0Promise キュー

解決策 D:dynamic import() を使用する

CJS プロジェクトから ESM パッケージを使いたい場合、dynamic import() を使用する方法があります。 これは非同期でモジュールを読み込む仕組みで、CJS と ESM の橋渡しができます。

dynamic import() の基本構文

require() の代わりに import() 関数を使用します(async​/​await が必要です)。

typescript// ❌ require() は使えない
// const chalk = require('chalk');
typescript// ✓ dynamic import() で読み込む
async function main() {
  const chalk = await import('chalk');

  console.log(chalk.default.blue('Hello World'));
}

main();

注意点:default プロパティへのアクセス

dynamic import() でインポートしたモジュールは、default プロパティ経由でアクセスする必要があります。

typescriptasync function colorize() {
  // chalk モジュールをインポート
  const chalkModule = await import('chalk');

  // default プロパティから実際の chalk を取得
  const chalk = chalkModule.default;

  console.log(chalk.green('Success!'));
  console.log(chalk.yellow('Warning!'));
}

より簡潔に書くには、分割代入を使います。

typescriptasync function colorize() {
  // default プロパティを直接取り出す
  const { default: chalk } = await import('chalk');

  console.log(chalk.green('Success!'));
}

トップレベルでの使用方法

トップレベル(関数の外)で dynamic import() を使う場合は、即時実行関数(IIFE)でラップします。

typescript// トップレベルで使用する場合
(async () => {
  const { default: chalk } = await import('chalk');

  console.log(chalk.red('Error occurred!'));
})();

TypeScript での型安全性

TypeScript で dynamic import() を使う場合、型情報も正しく取得できます。

typescriptasync function useChalk() {
  // 型情報も含めてインポート
  const { default: chalk } = await import('chalk');

  // TypeScript の型チェックが効く
  const message: string = chalk.blue('Typed message');
  console.log(message);
}

具体例

実践例 1:Express サーバーを ESM で構築

実際のプロジェクトで ESM を使用した Express サーバーの実装例をご紹介します。 この例では、TypeScript + ESM の組み合わせで、モダンな Web サーバーを構築します。

プロジェクト構成

plaintextmy-express-app/
├── src/
│   ├── server.ts          # メインサーバーファイル
│   ├── routes/
│   │   └── api.ts         # API ルート
│   └── utils/
│       └── logger.ts      # ロガーユーティリティ
├── dist/                  # ビルド出力先
├── package.json
└── tsconfig.json

package.json の設定

ESM モードを有効にし、必要な依存関係をインストールします。

json{
  "name": "my-express-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js",
    "dev": "tsx watch src/server.ts"
  }
}
json{
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.10.0",
    "typescript": "^5.3.3",
    "tsx": "^4.7.0"
  }
}

主要な依存関係の説明:

  • express: Web フレームワーク本体
  • tsx: TypeScript を直接実行できる開発ツール(ESM 対応)
  • @types​/​*: TypeScript の型定義ファイル

tsconfig.json の設定

json{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2022",
    "moduleResolution": "bundler",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

src/utils/logger.ts の実装

ロガーユーティリティを ESM で実装します。

typescript// chalk v5(ESM 専用)をインポート
import chalk from 'chalk';
typescript// ログレベルの型定義
type LogLevel = 'info' | 'warn' | 'error' | 'success';
typescript// Logger クラスの実装
export class Logger {
  private formatMessage(
    level: LogLevel,
    message: string
  ): string {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
  }

  info(message: string): void {
    console.log(
      chalk.blue(this.formatMessage('info', message))
    );
  }

  warn(message: string): void {
    console.log(
      chalk.yellow(this.formatMessage('warn', message))
    );
  }

  error(message: string): void {
    console.log(
      chalk.red(this.formatMessage('error', message))
    );
  }

  success(message: string): void {
    console.log(
      chalk.green(this.formatMessage('success', message))
    );
  }
}
typescript// デフォルトエクスポート
export default new Logger();

コードのポイント:

  • chalk を ESM の import 文で読み込んでいます
  • クラスとインスタンスの両方をエクスポートし、使いやすくしています
  • TypeScript の型定義で安全性を確保しています

src/routes/api.ts の実装

API ルートを定義します。

typescript// Express の Router をインポート
import { Router, Request, Response } from 'express';
import logger from '../utils/logger.js'; // ← .js 拡張子が必要
typescript// Router インスタンスの作成
const router = Router();
typescript// GET /api/hello エンドポイント
router.get('/hello', (req: Request, res: Response) => {
  logger.info('Hello endpoint accessed');

  res.json({
    message: 'Hello, ESM World!',
    timestamp: new Date().toISOString(),
  });
});
typescript// POST /api/data エンドポイント
router.post('/data', (req: Request, res: Response) => {
  const { name } = req.body;

  if (!name) {
    logger.warn('Name parameter missing');
    return res
      .status(400)
      .json({ error: 'Name is required' });
  }

  logger.success(`Data received: ${name}`);
  res.json({ received: name });
});
typescript// Router のエクスポート
export default router;

src/server.ts の実装

メインサーバーファイルを作成します。

typescript// 必要なモジュールをインポート
import express, {
  Express,
  Request,
  Response,
} from 'express';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
typescript// ローカルモジュールをインポート(.js 拡張子必須)
import apiRoutes from './routes/api.js';
import logger from './utils/logger.js';
typescript// __dirname の代替実装(ESM では必須)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
typescript// Express アプリケーションの初期化
const app: Express = express();
const PORT = process.env.PORT || 3000;
typescript// ミドルウェアの設定
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
typescript// ルートの登録
app.use('/api', apiRoutes);
typescript// ルートエンドポイント
app.get('/', (req: Request, res: Response) => {
  res.send('ESM Express Server is running!');
});
typescript// サーバー起動
app.listen(PORT, () => {
  logger.success(`Server is running on port ${PORT}`);
  logger.info(`Access: http://localhost:${PORT}`);
});

実行方法

以下のコマンドでサーバーを起動できます。

bash# 依存関係のインストール
yarn install

# 開発モードで起動(ホットリロード有効)
yarn dev

# 本番用ビルド
yarn build

# ビルド後のファイルを実行
yarn start

実践例 2:CJS プロジェクトで ESM パッケージを使う

既存の CJS プロジェクトを維持しながら、ESM 専用パッケージ(chalk v5)を使用する例です。 dynamic import() を活用することで、移行コストを最小限に抑えられます。

package.json の設定

CJS モードを維持します(type フィールドなし)。

json{
  "name": "cjs-project",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
json{
  "dependencies": {
    "chalk": "^5.3.0"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.3"
  }
}

tsconfig.json の設定

CJS 形式で出力します。

json{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2020",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "strict": true
  }
}

重要: targetES2020 以上に設定することで、async​/​await と dynamic import() が使えます。

src/utils/colorLogger.ts の実装

dynamic import() を使って chalk を読み込みます。

typescript// Chalk の型定義をインポート
import type { ChalkInstance } from 'chalk';
typescript// chalk インスタンスをキャッシュする変数
let chalkInstance: ChalkInstance | null = null;
typescript// chalk を遅延ロードする関数
async function getChalk(): Promise<ChalkInstance> {
  if (!chalkInstance) {
    // dynamic import() で ESM パッケージを読み込む
    const chalkModule = await import('chalk');
    chalkInstance = chalkModule.default;
  }
  return chalkInstance;
}

キャッシュのメリット: 初回のみ import を実行し、2 回目以降は保存済みのインスタンスを再利用することで、パフォーマンスが向上します。

typescript// ロガー関数の実装
export async function logInfo(
  message: string
): Promise<void> {
  const chalk = await getChalk();
  console.log(chalk.blue(`[INFO] ${message}`));
}

export async function logError(
  message: string
): Promise<void> {
  const chalk = await getChalk();
  console.log(chalk.red(`[ERROR] ${message}`));
}

export async function logSuccess(
  message: string
): Promise<void> {
  const chalk = await getChalk();
  console.log(chalk.green(`[SUCCESS] ${message}`));
}

src/index.ts の実装

メインファイルで、作成したロガーを使用します。

typescript// ロガー関数をインポート
import {
  logInfo,
  logError,
  logSuccess,
} from './utils/colorLogger';
typescript// メイン処理(async 関数)
async function main() {
  await logInfo('アプリケーションを起動しています...');

  try {
    // 何らかの処理
    await processData();

    await logSuccess('処理が完了しました!');
  } catch (error) {
    await logError(`エラーが発生しました: ${error}`);
  }
}
typescript// データ処理のサンプル関数
async function processData(): Promise<void> {
  await logInfo('データを処理中...');

  // 処理をシミュレート
  await new Promise((resolve) => setTimeout(resolve, 1000));

  await logInfo('データ処理が完了しました');
}
typescript// プログラムのエントリーポイント
main().catch(console.error);

実装のポイント:

  • すべてのロガー関数が async なので、呼び出し時に await が必要です
  • エラーハンドリングも非同期に対応しています
  • CJS プロジェクトでありながら、最新の ESM パッケージを使用できています

実践例 3:エラー発生時のデバッグ方法

実際に ERR_REQUIRE_ESM エラーが発生した場合の、体系的なデバッグ手順をご紹介します。

エラーメッセージの解読

bashError [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/chalk/source/index.js from /project/dist/logger.js not supported.
Instead change the require of /node_modules/chalk/source/index.js in /project/dist/logger.js to a dynamic import() which is available in all CommonJS modules.

このエラーメッセージから、以下の情報が読み取れます。

#情報内容対処のヒント
1エラーコードERR_REQUIRE_ESMrequire() で ESM を読もうとしている
2問題のパッケージchalk/source/index.jschalk が ESM 専用
3エラー発生箇所/project/dist/logger.jslogger.js で require() している
4推奨される解決策dynamic import() を使用async/await で import() に変更

デバッグステップ 1:package.json を確認

プロジェクトのモジュールタイプを確認します。

bash# package.json の type フィールドを確認
cat package.json | grep "type"

結果の解釈:

json// ケース 1: フィールドなし → CJS モード
{
  "name": "my-project"
  // "type" フィールドがない
}
json// ケース 2: commonjs → CJS モード
{
  "type": "commonjs"
}
json// ケース 3: module → ESM モード
{
  "type": "module"
}

デバッグステップ 2:tsconfig.json を確認

TypeScript の出力形式を確認します。

json{
  "compilerOptions": {
    "module": "???" // ← ここをチェック
  }
}
#module の値出力形式説明
1CommonJSCJSrequire() / module.exports を生成
2ESNextESMimport / export を生成
3ES2015, ES2020 などESMimport / export を生成
4NodeNext自動判別package.json の type に従う

デバッグステップ 3:ビルド後のコードを確認

トランスパイル後のファイルで、実際にどのような構文が使われているか確認します。

bash# ビルドされたファイルの先頭を確認
head -n 20 dist/logger.js

CJS として出力されている場合:

javascript'use strict';
// ❌ require() が使われている
const chalk = require('chalk');

module.exports = {
  logInfo: function (message) {
    // ...
  },
};

ESM として出力されている場合:

javascript// ✓ import 文が使われている
import chalk from 'chalk';

export function logInfo(message) {
  // ...
}

デバッグステップ 4:依存パッケージのバージョンを確認

問題のパッケージが ESM 専用かどうかを確認します。

bash# インストールされているバージョンを確認
yarn list chalk

# または
npm list chalk

結果例:

bash└─ chalk@5.3.0  # ← v5 以降は ESM 専用

解決フローチャート

以下の図で、デバッグから解決までの全体像を把握しましょう。

mermaidflowchart TD
  error["ERR_REQUIRE_ESM<br/>エラー発生"] --> check1["ステップ1<br/>package.json 確認"]

  check1 --> is_esm{"type: module<br/>になっている?"}

  is_esm -->|いいえ(CJS)| check2["ステップ2<br/>tsconfig.json 確認"]
  is_esm -->|はい(ESM)| check_code1["ビルド後のコードに<br/>require() が残っていないか確認"]

  check2 --> module_type{"module の値は?"}

  module_type -->|CommonJS| choice["解決策を選択"]
  module_type -->|ESNext| mismatch["設定の不一致を修正<br/>package.json に<br/>type: module を追加"]

  choice --> option1["オプション A<br/>ESM へ完全移行"]
  choice --> option2["オプション B<br/>dynamic import() 使用"]
  choice --> option3["オプション C<br/>パッケージを<br/>旧バージョンに固定"]

  option1 --> fix1["1. package.json に<br/>type: module 追加<br/>2. 拡張子 .js を明示<br/>3. __dirname を代替実装"]
  option2 --> fix2["require() を<br/>await import() に書き換え"]
  option3 --> fix3["package.json で<br/>バージョンを固定"]

  fix1 --> success["✓ 解決"]
  fix2 --> success
  fix3 --> success
  mismatch --> success
  check_code1 --> success

  style error fill:#f8d7da
  style success fill:#d4edda

図で理解できる要点:

  • エラー発生時は、まず package.json と tsconfig.json の設定を確認
  • 設定の不一致があれば修正し、なければ 3 つの解決策から選択
  • どの選択肢も有効だが、将来性を考えると ESM への完全移行が推奨される

まとめ

本記事では、TypeScript プロジェクトで頻発する「ERR_REQUIRE_ESM」と「import 文が使えない」エラーについて、根本原因から具体的な解決策まで詳しく解説しました。

重要なポイント:

  • JavaScript のモジュールシステムは、CJS から ESM へ移行している過渡期にあります
  • エラーの根本原因は、package.jsontype フィールドと tsconfig.jsonmodule 設定の不一致です
  • 解決策は状況に応じて選択でき、新規プロジェクトでは ESM への完全移行がおすすめです
  • 既存プロジェクトでは、dynamic import() や旧バージョンの利用も有効な選択肢となります

解決策の選び方:

#状況推奨される解決策メリット
1新規プロジェクトESM への完全移行最新パッケージが使える、将来性が高い
2小〜中規模の既存プロジェクトESM への完全移行根本的に問題を解決できる
3大規模な既存プロジェクトdynamic import()移行コストが低い、段階的に対応可能
4機能要件が満たせる場合旧バージョン使用既存コードの変更が不要

ESM は JavaScript の標準仕様であり、今後のエコシステムの中心となっていきます。 早めに ESM に対応しておくことで、将来的な互換性問題を回避し、最新のパッケージやツールを活用できるようになるでしょう。

エラーに遭遇した際は、本記事のデバッグステップを参考に、あなたのプロジェクトに最適な解決策を見つけてください。

関連リンク