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.exports | import / export |
| 2 | 読み込み | 同期 | 非同期 |
| 3 | ファイル拡張子 | .js / .cjs | .mjs / .js (package.json で指定) |
| 4 | Node.js 対応 | 初期から対応 | Node.js 12 以降 |
| 5 | Tree 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.jsonのtypeフィールドが一致していない場合 - トランスパイル後のコードが想定と異なるモジュール形式で出力される
以下の図で、エラーが発生する典型的なフローを確認しましょう。
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.jsonのtypeフィールドでモジュールシステムを判断する - CJS モードでは
import文や ESM パッケージが使えない - ESM モードでは
require()が使えない
なぜこの問題が起こるのか
この問題の根本原因は、JavaScript エコシステムの「移行期」にあります。 npm パッケージの作者が ESM への移行を進める一方で、既存のプロジェクトは CJS のままというケースが多いのです。
特に以下のパッケージは ESM 専用となり、多くの開発者を悩ませています。
| # | パッケージ名 | ESM 専用になったバージョン | 用途 |
|---|---|---|---|
| 1 | chalk | v5.0.0 以降 | ターミナル文字の色付け |
| 2 | node-fetch | v3.0.0 以降 | HTTP リクエスト |
| 3 | execa | v6.0.0 以降 | プロセス実行 |
| 4 | got | v12.0.0 以降 | HTTP クライアント |
| 5 | p-queue | v7.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"
}
}
各設定項目の意味は以下の通りです。
| # | 設定項目 | 推奨値 | 説明 |
|---|---|---|---|
| 1 | module | ESNext | ESM 形式でコードを出力 |
| 2 | target | ES2020 以降 | async/await などの新機能を使用可能に |
| 3 | moduleResolution | node または bundler | モジュール解決方法を指定 |
| 4 | esModuleInterop | true | CJS との相互運用性を向上 |
| 5 | allowSyntheticDefaultImports | true | default 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 専用初版 | 備考 |
|---|---|---|---|---|
| 1 | chalk | v4.1.2 | v5.0.0 | ターミナル色付け |
| 2 | node-fetch | v2.7.0 | v3.0.0 | HTTP クライアント |
| 3 | execa | v5.1.1 | v6.0.0 | プロセス実行 |
| 4 | got | v11.8.6 | v12.0.0 | HTTP リクエスト |
| 5 | p-queue | v6.6.2 | v7.0.0 | Promise キュー |
解決策 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
}
}
重要: target を ES2020 以上に設定することで、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_ESM | require() で ESM を読もうとしている |
| 2 | 問題のパッケージ | chalk/source/index.js | chalk が ESM 専用 |
| 3 | エラー発生箇所 | /project/dist/logger.js | logger.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 の値 | 出力形式 | 説明 |
|---|---|---|---|
| 1 | CommonJS | CJS | require() / module.exports を生成 |
| 2 | ESNext | ESM | import / export を生成 |
| 3 | ES2015, ES2020 など | ESM | import / export を生成 |
| 4 | NodeNext | 自動判別 | 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.jsonのtypeフィールドとtsconfig.jsonのmodule設定の不一致です - 解決策は状況に応じて選択でき、新規プロジェクトでは ESM への完全移行がおすすめです
- 既存プロジェクトでは、dynamic import() や旧バージョンの利用も有効な選択肢となります
解決策の選び方:
| # | 状況 | 推奨される解決策 | メリット |
|---|---|---|---|
| 1 | 新規プロジェクト | ESM への完全移行 | 最新パッケージが使える、将来性が高い |
| 2 | 小〜中規模の既存プロジェクト | ESM への完全移行 | 根本的に問題を解決できる |
| 3 | 大規模な既存プロジェクト | dynamic import() | 移行コストが低い、段階的に対応可能 |
| 4 | 機能要件が満たせる場合 | 旧バージョン使用 | 既存コードの変更が不要 |
ESM は JavaScript の標準仕様であり、今後のエコシステムの中心となっていきます。 早めに ESM に対応しておくことで、将来的な互換性問題を回避し、最新のパッケージやツールを活用できるようになるでしょう。
エラーに遭遇した際は、本記事のデバッグステップを参考に、あなたのプロジェクトに最適な解決策を見つけてください。
関連リンク
articleTypeScript タプル/配列操作チートシート:`Length`・`Push`・`Zip`・`Chunk`を型で書く
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleTypeScript Null 安全戦略の比較検証:ts-reset vs strictNullChecks vs noUncheckedIndexedAccess
articleESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す
articleTypeScript 型安全なフィーチャーフラグ設計:判別可能共用体で運用事故を防ぐ
articleTypeScript satisfies 演算子の実力:型の過剰/不足を一発検知する実践ガイド
articleZod 合成パターン早見表:`object/array/tuple/record/map/set/intersection` 実例集
articleバックアップ戦略の決定版:WordPress の世代管理/災害復旧の型
articleYarn 運用ベストプラクティス:lockfile 厳格化・frozen-lockfile・Bot 更新方針
articleWebSocket のペイロード比較:JSON・MessagePack・Protobuf の速度とコスト検証
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWebRTC SDP 用語チートシート:m=・a=・bundle・rtcp-mux を 10 分で総復習
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来