- 技術 💻
Cannot use import statementの原因と直し方|Jest・TS
結論
このエラーは、import 文を書いたファイルが「ES モジュールではなく CommonJS(または普通のスクリプト)」として読み込まれたときに出ます。Node.js なら package.json に "type": "module" を書くか拡張子を .mjs に、ブラウザなら <script type="module"> に、Jest なら Babel で変換するか ESM モードで動かします。
この記事でわかること
- Node.js・ブラウザ・Jest・TypeScript 別の原因と直し方
- 同じ原因のエラー13通り・近いエラー10通りを再現した原文と、直したあとの出力
- Jest 29 と Jest 30 で文言が変わった点と、どの直し方を選ぶかの判断表
この記事で実際に動かしたもの:Node.js 22.23.4 で package.json・拡張子・フラグを変えた20個以上の小さなプロジェクト、Jest 30.5.2 と 29.7.0、TypeScript 6.0.3(tsc)・ts-node 10.9.2・tsx 4.23.12・ts-jest 29.4.15、127.0.0.1 の自作サーバーで配信したページを開いた Chromium 153。
エラーが出た場所で直し方が分かれます。まず「どこで出たか」を決めると、読む節が1つに絞れます。
flowchart LR
W{import で<br>SyntaxError<br>どこで出た?} -->|node| N[type を module<br>か .mjs に]
W -->|ブラウザ| B[script に<br>type=module]
W -->|Jest| J[Babel で変換<br>か ESM モード]
W -->|tsc| T[module を<br>nodenext に]
やりたいことごとに、いちばん早い方法を先にまとめます。
| やりたいこと | いちばん早い方法 | 詳しい節 |
|---|---|---|
| node app.js で出た | package.json に "type": "module" を足す | Node.jsで出る原因1 |
| package.json を変えられない | そのファイルだけ .mjs にする・import() で読む | Node.jsで直す4つの方法 |
| ブラウザのコンソールで出た | script タグに type="module" を付ける | ブラウザで出る原因と直し方 |
| Jest で出た(自分のテスト) | @babel/preset-env の babel.config.js を置く | Jestで出る原因と直し方 |
| Jest で出た(node_modules の中) | transformIgnorePatterns でそのパッケージだけ変換する | Jestで出る原因と直し方 |
| tsc でビルドした JS で出た | tsconfig の module を nodenext にする | TypeScriptで出る原因と直し方 |
| ts-node で出た・何も表示されない | tsx で動かす、または module を CommonJS 系にする | TypeScriptで出る原因と直し方 |
| 直したら別のエラーが出た | require・__dirname・拡張子を ESM の書き方に直す | 直したあとに出る近いエラー |
確認環境:Ubuntu 24.04.5 LTS の上で、Node.js 22.23.4(npm 10.9.9)を使い、作業用のフォルダーに package.json の "type" の有無・値、拡張子(.js・.mjs・.cjs・.ts)、--no-experimental-detect-module などのフラグを変えた小さなプロジェクトを作って、エラーの原文と直したあとの出力を記録しました。テストは Jest 30.5.2(babel-jest 30.5.2・@babel/core 8.1.0・@babel/preset-env 8.0.7)と Jest 29.7.0、TypeScript は typescript 6.0.3(tsc)・ts-node 10.9.2・tsx 4.23.12・ts-jest 29.4.15 で実行しています。ブラウザは Python 3.13.18 の http.server を 127.0.0.1 に立ててページを配信し、Playwright 1.63.0 の Chromium 153.0.8010.12 で開いてコンソールの表示を集めました。Node.js 20 系など別の版の挙動は、nodejs.org からの取得がこの環境では許可されなかったため、Node.js 公式ドキュメントの版の履歴で確かめています。Firefox・Safari の文言は MDN、Jest の ESM 対応は Jest 公式ドキュメント(30.5)、TypeScript の module 設定は TypeScript 公式ハンドブックで確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cannot use import statementの原因と直し方|Jest・TS」
Cannot use import statement outside a moduleの意味(エラー原文)
この節では、エラー文がそれぞれの言葉で何を言っているのかと、実際にどう表示されるのかが分かります。
直訳すると「モジュールの外では import 文を使えません」です。JavaScript には、ファイルを読み込む仕組みが2つあります。import / export を使う ES モジュール(ESM)と、require() / module.exports を使う CommonJS(CJS)です。import 文は ESM の中でしか書けないので、実行する側がそのファイルを CommonJS や普通のスクリプトとして読み込むと、1行目の import で構文エラー(SyntaxError)になります。
Node.js 22.23.4 で、package.json に "type": "commonjs" があるフォルダーの app.js に import を書いて実行すると、次のように出ました。
$ node app.js
(node:9016) Warning: Failed to load the ES module: /dev/shm/t285/lab/n2/app.js. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/dev/shm/t285/lab/n2/app.js:1
import { add } from './math.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at wrapSafe (node:internal/modules/cjs/loader:1713:18)
at Module._compile (node:internal/modules/cjs/loader:1755:20)
...
Node.js v22.23.4
読むところは3つです。
- Warning の行:Node.js 22 では、エラーの前に直し方そのものが表示されました。
^^^^^^の位置:どのファイルの import でつまずいたか。自分のファイル・node_modules・設定ファイルのどれかで直す場所が変わります。- スタックの
cjs/loader:CommonJS の読み込み器(loader)で止まった、つまり CommonJS として読まれたという証拠です。
ブラウザでは同じ原因でも文言が違います。MDN は次の3つを挙げています。
| 実行する環境 | 表示される文言 |
|---|---|
| Chrome・Edge・Node.js(V8) | SyntaxError: Cannot use import statement outside a module |
| Firefox | SyntaxError: import declarations may only appear at top level of a module |
| Safari | SyntaxError: Unexpected identifier 'x'. import call expects one or two arguments. |
Firefox の文言は「import 宣言はモジュールの最上位にだけ書ける」という意味で、if 文や関数の中に import 文を書いたときにも出ます。その場合は import()(動的インポート)を使います。
原因の一覧表と、最初に確かめる3つのこと
この節では、このエラーの原因が環境ごとに何通りあるかと、どれに当たるかをすぐに確かめる方法が分かります。
原因は「import を書いたファイル」と「そのファイルをどう読むかの設定」が食い違っていることです。手元で再現できた原因を、環境別にまとめました。
| 環境 | 原因 | 手元の結果 | 直し方 |
|---|---|---|---|
| Node.js | package.json が "type": "commonjs" | このエラー | "type": "module"・.mjs |
| Node.js | 拡張子が .cjs | このエラー | .mjs にする・require に書き換える |
| Node.js | 構文の自動判定が無い版・フラグで無効 | このエラー | "type" を明示する |
| ブラウザ | script タグに type="module" が無い | このエラー | type="module" を付ける |
| ブラウザ | 普通の script の中に import を直書き | このエラー | type="module" を付ける |
| Jest 29 | テストを CommonJS に変換していない | このエラー | Babel・ESM モード |
| Jest 30 | 同上・node_modules の ESM | Must use import to load ES Module | Babel・transformIgnorePatterns |
| Jest | jest.config.js を ESM で書いた | このエラー | jest.config.mjs にする |
| TypeScript | module: ESNext で出力して node で実行 | このエラー | module: nodenext |
| TypeScript | node で .ts を直接実行(CommonJS 扱い) | このエラー | "type": "module"・.mts |
どれに当たるかは、次の3つで確かめられます(手元で実行した結果)。
Node.js の版を見る
node -v を実行します。v22.7.0 以降・v20.19.0 以降なら、"type" が無いときに import を見つけて ESM として読み直す自動判定が既定で動きます。それより前の版では、"type" が無い .js は必ず CommonJS です。
package.json の "type" を見る
プロジェクトのフォルダーで npm pkg get type を実行します。手元では "module"・"commonjs" のどちらか、書いていなければ {} と表示されました。いちばん近い親フォルダーの package.json が効きます。
そのファイルがどちらで動くかを見る
同じフォルダーに console.log(typeof module === 'undefined' ? 'ESM' : 'CommonJS'); だけを書いた which.js を置いて node which.js を実行します。"type": "module" のフォルダーでは ESM、"commonjs" と "type" 無しでは CommonJS と表示されました。
ブラウザでは、開発者ツール(F12)の Elements タブで script タグに type="module" があるかを見ます。
Node.jsで出る原因1:package.jsonの"type"がcommonjs
この節では、Node.js で最も多い原因である package.json の "type" と、直し方・直したあとの出力が分かります。
Node.js は、.js ファイルを ESM と CommonJS のどちらで読むかを、いちばん近い package.json の "type" で決めます。"commonjs" と書いてあれば、その下の .js はすべて CommonJS です。公式ドキュメントは、"type" が無い場合も .js は CommonJS として扱うと書いています(ただし後で説明する自動判定があります)。
再現に使ったのは、次の2つのファイルです。
// math.js
export const add = (a, b) => a + b;
// app.js
import { add } from './math.js';
console.log('1 + 2 =', add(1, 2));
{
"name": "n2",
"type": "commonjs"
}
この状態で node app.js を実行すると、前の節の原文どおり SyntaxError になりました。package.json の "type" を "module" に変えると、何も書き換えずに動きました。
$ cat package.json
{
"name": "f2",
"type": "module"
}
$ node app.js
1 + 2 = 3
モノレポ(1つのリポジトリに複数のパッケージ)では、上の階層の別の package.json の "type" が効いていることがあります。npm pkg get type はファイルのあるフォルダーで実行します。
"type": "module" に変える前に見ること
"type" はフォルダーの下の .js すべてに効く
ESM と CommonJS の仕組みの違い(読み込み方の対応表・版ごとの挙動)は、関連記事「Node.js ESMとCommonJSの違い|import混在の解決表」で扱っています。この記事はエラーが出たときの入口として、原因の切り分けと直し方に絞ります。
Node.jsで出る原因2:.cjs・自動判定の有無・require との混在
この節では、"type" 以外で Node.js がファイルを CommonJS として読む場合と、版による違い、自動判定にかかる時間が分かります。
拡張子 .cjs のファイルは、package.json に関係なく必ず CommonJS です。.cjs の中に import 文を書くと、どの設定でも同じエラーになりました。
$ node app.cjs
(node:9040) Warning: Failed to load the ES module: /dev/shm/t285/lab/n4/app.cjs. Make sure to set "type": "module" in the nearest package.json file or use the .mjs extension.
/dev/shm/t285/lab/n4/app.cjs:1
import { add } from './math.mjs';
^^^^^^
SyntaxError: Cannot use import statement outside a module
"type" が無いときは自動判定がある(Node.js 22.7 以降・20.19 以降)
Node.js の公式ドキュメントでは、"type" が無い(または package.json が無い).js は「あいまいな入力」とされ、CommonJS として読めないときに import・export などの ESM の構文を見つけると、ESM として読み直します。この構文の自動判定(Syntax detection)は v22.7.0・v20.19.0 で既定で有効になりました。手元の 22.23.4 で "type" の無い package.json のフォルダーで実行すると、エラーにはならず、代わりに警告が出ました。
$ node app.js
1 + 2 = 3
(node:9024) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///dev/shm/t285/lab/n3/app.js is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to /dev/shm/t285/lab/n3/package.json.
つまり同じコードでも、Node.js 20.18 以前や 22.6 以前ではこのエラーになり、22.7 以降では動きます。「手元では動くのに CI(自動テストのサーバー)ではエラー」なら版の違いを疑います。自動判定を切る --no-experimental-detect-module を付けると、以前の版と同じくエラーになりました。
$ node --no-experimental-detect-module app.js
/dev/shm/t285/lab/n1/app.js:1
import { add } from './math.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
自動判定に頼ると遅くなるか(実測)
警告に「performance overhead(処理の余分な負担)」とあるので、起動にかかる時間を測りました。各条件で23回起動し、最初の3回を除いた中央値です。
| 条件 | ファイル | 起動の中央値 |
|---|---|---|
| "type": "module" | 2ファイル・数行 | 34.1 ms |
| "type" 無し(自動判定) | 2ファイル・数行 | 33.8 ms |
| 拡張子 .mjs | 2ファイル・数行 | 32.9 ms |
| "type": "module" | 約1.4MB・3万行のモジュール | 150.7 ms・152.7 ms |
| "type" 無し(自動判定) | 約1.4MB・3万行のモジュール | 169.0 ms・173.3 ms |
大きなファイルでは読み直しの分だけ遅くなりました。公式ドキュメントも、CommonJS だけのパッケージでも "type" を書くよう勧めています。自動判定はエラーを避ける助けにはなりますが、"type" を明示するのが確実です。
require と import を1つのファイルに混ぜた場合
"type" の無いフォルダーで、1つのファイルに require と import を両方書くと、自動判定で ESM として読み直された結果、今度は require が使えなくなりました。
$ node app.js
file:///dev/shm/t285/lab/n5/app.js:1
const fs = require('node:fs');
^
ReferenceError: require is not defined in ES module scope, you can use import instead
1つのファイルの中では、どちらか一方にそろえます(直し方は「直したあとに出る近いエラー」の節)。
Node.jsで直す4つの方法(type・.mjs・import()・require)
この節では、Node.js でこのエラーを直す方法を4つ、それぞれ直したあとの出力つきで比べられます。
プロジェクト全体を ESM にするなら "type": "module"、一部だけなら .mjs、CommonJS のまま ESM を使うなら import() か require() です。4つとも手元で動きました。
"type": "module"
package.json に1行足す。フォルダーの下の .js がすべて ESM になる。
新しいプロジェクト向け
拡張子を .mjs
そのファイルだけ ESM にする。import する側も ./math.mjs と書く。
一部だけ ESM にしたい
import() で読む
CommonJS のファイルから、動的インポートで ESM を読む。結果は Promise で返る。
どの版でも使える
require() で読む
Node.js 22.12 以降・20.19 以降は、require() で ESM を読める。
トップレベル await は不可
方法2:拡張子を .mjs にする
"type": "commonjs" のまま、ファイルを .mjs に変えると動きました。
$ cat app.mjs
import { add } from './math.mjs';
console.log('1 + 2 =', add(1, 2));
$ node app.mjs
1 + 2 = 3
方法3:CommonJS のまま import() で読む
import 文(静的インポート)は ESM でしか書けませんが、import()(動的インポート)は CommonJS の中でも使えます。公式ドキュメントも、import() はファイルを ESM 扱いにする理由にならないと書いています。
// app.js("type": "commonjs" のまま)
(async () => {
const { add } = await import('./math.mjs');
console.log('1 + 2 =', add(1, 2));
})();
$ node app.js
1 + 2 = 3
方法4:require() で ESM を読む(Node.js 22.12 以降)
Node.js の公式ドキュメントでは、require() で ESM を読む機能は v22.12.0・v20.19.0 でフラグ無しで使えるようになり、v22.13.0 以降は実験的機能の警告も出なくなりました。手元の 22.23.4 では、警告なしで動きました。
$ cat req.js
const { add } = require('./math.mjs');
console.log('1 + 2 =', add(1, 2));
$ node req.js
1 + 2 = 3
$ node -p "process.features.require_module"
true
ただし、読み込む ESM(またはその先)にトップレベル await(関数の外の await)があると、別のエラーになりました。この場合は import() を使います。
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.
From /dev/shm/t285/lab/f4/req2.js
Requiring /dev/shm/t285/lab/f4/tla.mjs
code: 'ERR_REQUIRE_ASYNC_MODULE'
node -e で1行だけ試すとき
node -e に渡した文字列も「あいまいな入力」なので、22.23.4 では import を含む1行がそのまま動きました。どの版でも確実に ESM として動かすなら --input-type=module を付けます。
$ node --input-type=module -e "import os from 'node:os'; console.log(os.platform())"
linux
ブラウザで出る原因と直し方(script type="module")
この節では、ブラウザのコンソールにこのエラーが出る原因と、直したあとに出やすい3つのエラーが分かります。
ブラウザでは、<script> タグに type="module" が無いと、そのファイルは普通のスクリプトとして読まれ、import 文でエラーになります。127.0.0.1 の自作サーバーで次のページを配信し、Chromium 153 で開きました。
<!-- b1.html:エラーになる -->
<p id="out">-</p>
<script src="app.js"></script>
<!-- b2.html:動く -->
<p id="out">-</p>
<script type="module" src="app.js"></script>
Playwright でページのエラーとコンソールを集めた結果です。b2 では app.js が math.js を読み込み、画面の文字が書き換わりました。
=== b1 out=-
[pageerror] SyntaxError: Cannot use import statement outside a module
=== b4 out=- (普通の script タグの中に import を直接書いた)
[pageerror] SyntaxError: Cannot use import statement outside a module
=== b2 out=1 + 2 = 3
Chrome のコンソールでは、文言の前に「Uncaught」が付いて赤く表示されます。type="module" を付けると、次の2つが変わります。
- file:// では動かない:HTML ファイルをダブルクリックで開くと CORS のエラーになります。
- サーバーの Content-Type が厳しく見られる:JavaScript の MIME タイプで返さないと読み込まれません。
直したあとに出たエラー(file://・MIME・裸の指定子)
手元で再現した原文です。
=== file:///.../b2.html を直接開いた
[console.error] Access to script at 'file:///dev/shm/t285/lab/web/app.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-untrusted, data, http, https.
[console.error] Failed to load resource: net::ERR_FAILED
=== b7:text/plain で配信したファイルを type="module" で読んだ
[console.error] Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec.
=== b5:import confetti from 'confetti-lib'(パッケージ名だけで import)
[pageerror] TypeError: Failed to resolve module specifier "confetti-lib". Relative references must start with either "/", "./", or "../".
| 出たエラー | 原因 | 直し方 |
|---|---|---|
| blocked by CORS policy(origin 'null') | file:// で開いた | ローカルサーバー(npx vite・python -m http.server など)で開く |
| Failed to load module script … MIME type | サーバーが .js を text/plain などで返した | text/javascript で返すようサーバーを設定する |
| Failed to resolve module specifier | パッケージ名だけの import はブラウザでは解決できない | import map を書く・Vite などのバンドラーを使う |
パッケージ名だけの import は、import map(どの名前をどのファイルに対応させるかの表)を書くと動きました。
<script type="importmap">
{ "imports": { "confetti-lib": "./vendor/confetti.js" } }
</script>
<script type="module" src="bare.js"></script>
=== b6 out=undefined
[console.log] confetti-lib loaded
Web Worker では、MDN のとおり new Worker('worker.js', { type: 'module' }) と指定します。ブラウザでの import / export と import map の書き方は、関連記事「JavaScript import/export入門|ESMとCommonJSの違い」で扱っています。
Jestで出る原因と直し方(Jest 29と30で文言が違う)
この節では、Jest でこのエラーが出る3つの場面と、Babel・transformIgnorePatterns・ESM モードの3つの直し方が分かります。
Jest は、標準ではテストファイルを CommonJS として実行します。import を書いたテストを CommonJS に変換する設定が無いと失敗します。同じテストを Jest 29.7.0 と Jest 30.5.2 で動かすと、表示が変わっていました。
// math.test.js
import { add } from './math.js';
test('add', () => {
expect(add(1, 2)).toBe(3);
});
Jest 29.7.0 では、よく検索されるこの形でした。
FAIL a/math.test.js
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
...
Details:
/dev/shm/t285/lab/jest29/a/math.test.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { add } from './math.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
Jest 30.5.2 では、同じ原因で次の文言になり、直し方が3つ示されました。
FAIL j1/math.test.js
● Test suite failed to run
Must use import to load ES Module: /dev/shm/t285/lab/tools/j1/math.test.js
The file contains ESM syntax (import/export) that could not be executed as CommonJS. Either:
- Configure a transform (e.g. babel-jest) that compiles this file to CommonJS (see https://jestjs.io/docs/code-transformation)
- If the file is in "node_modules", allow it to be transformed by adjusting "transformIgnorePatterns" (see https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)
- Use Node v24.9+ where Jest supports require(esm) natively (see https://jestjs.io/docs/ecmascript-modules#require-of-esm)
文言は違っても、原因と直し方は同じです。Jest 30 で「Cannot use import statement outside a module」と検索しても見つからないときは、この「Must use import to load ES Module」で探すと近い情報が見つかります。
直し方1:Babel で CommonJS に変換する(いちばん手軽)
Jest に同梱される babel-jest は、Babel の設定ファイルがあればそれを使って変換します。@babel/core と @babel/preset-env を入れ、次のファイルを置くと通りました。
npm i -D babel-jest @babel/core @babel/preset-env
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
};
PASS j2/math.test.js
✓ add (7 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
直し方2:node_modules の ESM パッケージだけ変換する
自分のテストは変換できていても、import した先のパッケージが ESM だけで配布されていると、node_modules の中のファイルで止まります。Jest は標準で node_modules を変換しないためです。ESM だけのパッケージを作業用に用意して再現すると、止まった場所が node_modules の中と表示されました。
Must use import to load ES Module: /dev/shm/t285/lab/tools/node_modules/esm-only/index.js
...
> 1 | import { greet } from 'esm-only';
| ^
transformIgnorePatterns(変換しないファイルの条件)を書き換え、そのパッケージだけ変換の対象に戻すと通りました。複数あるときは (?!pkg-a|pkg-b) のように並べます。
// jest.config.js
module.exports = {
transformIgnorePatterns: ['/node_modules/(?!esm-only/)'],
};
PASS j3/greet.test.js
✓ greet (3 ms)
直し方3:Jest を ESM のまま動かす
Jest 公式ドキュメント(30.5)では、変換を止める transform: {} と、Node.js の --experimental-vm-modules を組み合わせると ESM のまま実行できると書かれています。フラグなしでは失敗し、付けると通りました(実験的機能の警告は出ます)。
// jest.config.js(package.json は "type": "module")
export default { transform: {} };
$ NODE_OPTIONS=--experimental-vm-modules npx jest
(node:12064) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
PASS j4/math.test.js
✓ add (3 ms)
ESM モードでは jest.mock が ESM に効かず、公式ドキュメントは jest.unstable_mockModule を案内しています。モックが多いテストは直し方1が向きます。Windows で NODE_OPTIONS を付けるときは、Jest 公式ドキュメントが cross-env を使う方法を案内しています。
設定ファイル(jest.config.js)で出る場合
jest.config.js を export default で書き、package.json が "type": "commonjs" だと、テストより前に設定ファイルの読み込みで同じエラーになりました。拡張子を .mjs に変える(jest.config.mjs)と通りました。next.config.js・eslint.config.js など、他のツールの設定ファイルでも考え方は同じで、エラーの ^^^^^^ が設定ファイルを指していないかを見ます。
/dev/shm/t285/lab/tools/jc/jest.config.js:1
import os from 'node:os';
^^^^^^
SyntaxError: Cannot use import statement outside a module
TypeScriptで出る原因と直し方(tsc・ts-node・tsx・ts-jest)
この節では、TypeScript のプロジェクトでこのエラーが出る原因と、tsconfig の module の選び方、実行の道具ごとの違いが分かります。
TypeScript では、tsconfig.json の module が出力する JavaScript の形を決めます。module を ESNext にすると出力に import 文が残り、package.json が "type": "commonjs" のまま node で実行するとエラーになりました。
$ cat tsconfig.json (抜粋)
"module": "ESNext",
"moduleResolution": "bundler",
$ npx tsc && head -1 dist/app.js
import { add } from './math';
$ node dist/app.js
/dev/shm/t285/lab/tools/ts1/dist/app.js:1
import { add } from './math';
^^^^^^
SyntaxError: Cannot use import statement outside a module
TypeScript 公式ハンドブックは、Node.js で動かすアプリ・ライブラリには module を node16・node18・nodenext のどれかにするよう勧め、esnext は「Node.js には使わない(バンドラー・Bun・tsx 向け)」としています。nodenext にすると、package.json の "type" に合わせて出力の形が自動で決まりました。
| tsconfig の module | package.json の type | 出力の1行目 | node で実行 |
|---|---|---|---|
| ESNext | commonjs | import { add } from './math'; | このエラー |
| NodeNext | commonjs | "use strict"(require に変換) | 1 + 2 = 3 |
| NodeNext | module(拡張子なし) | tsc がエラー TS2835 | ビルドできない |
| NodeNext | module(./math.js と書く) | import { add } from './math.js'; | 1 + 2 = 3 |
"type": "module" で nodenext にすると、相対パスの拡張子を省いた import を tsc が止めました。.ts のファイルを読む場合でも、import には出力後の名前(./math.js)を書きます。
src/app.ts(1,21): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'?
なお TypeScript 6.0.3 では、CommonJS の出力に以前よく使われた "moduleResolution": "node10" を指定すると、次のエラーでビルドが止まりました。CommonJS で出力したい場合も、module を NodeNext にして package.json を "type": "commonjs" にする組み合わせが、手元では警告なしで通りました。
tsconfig.json(5,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
node で .ts を直接実行した場合(型ストリップ)
Node.js 22.18.0 以降は、型の注釈を取り除いて .ts を直接実行できます(公式ドキュメントでは v23.6.0・v22.18.0 で既定で有効)。このとき .ts も .js と同じ規則で ESM か CommonJS かが決まり、tsconfig.json は読まれません。"type": "commonjs" のフォルダーで node src/app.ts を実行すると、同じエラーになりました。公式ドキュメントのとおり、"type": "module" にするか、拡張子を .mts(常に ESM)にします。
ts-node・tsx・ts-jest の違い
| 道具 | module: ESNext・type: commonjs での結果 | 手元での直し方 |
|---|---|---|
| ts-node 10.9.2 | 何も表示されず終了コード0(app.ts が実行されなかった) | module を CommonJS 系にすると「1 + 2 = 3」 |
| ts-node(型ストリップを切った場合) | TypeError: Unknown file extension ".ts" | 同上 |
| tsx 4.23.12 | 1 + 2 = 3(設定を変えずに動いた) | なし |
| ts-jest 29.4.15 | PASS(ESNext のままでも通った) | なし |
「エラーは出ないのに console.log が何も表示されない」ときも、ESM と CommonJS の食い違いを疑います。手元で設定を変えずに動いたのは tsx でした。TypeScript を Node.js で動かす道具の比較は関連記事「TypeScriptをNode.jsで実行|tsx・ts-node・型ストリップ」で、tsconfig の各項目の推奨値は関連記事「tsconfig設定一覧|TypeScript 7対応の推奨値とテンプレート」で扱っています。
直したあとに出る近いエラーの見分け方
この節では、このエラーを直した直後に出やすい別のエラーと、それぞれが何を意味するかが分かります。
ESM にそろえると、CommonJS でだけ使えたものが使えなくなります。手元で出た順に並べました。
| 出たエラー(原文の要点) | 意味 | 直し方 |
|---|---|---|
| ReferenceError: require is not defined in ES module scope | ESM の中で require を使った | import に書き換える・createRequire |
| ReferenceError: __dirname is not defined in ES module scope | ESM には __dirname・__filename が無い | import.meta.dirname・import.meta.filename |
| Error [ERR_MODULE_NOT_FOUND]: Cannot find module …/math | ESM では相対パスの拡張子を省けない | './math.js' と拡張子まで書く |
| Error [ERR_REQUIRE_ESM] | require() で ESM を読めない版 | Node.js 22.12 以降・20.19 以降にする・import() |
| Error [ERR_REQUIRE_ASYNC_MODULE] | require() した ESM にトップレベル await | import() で読む |
| error TS2835 | nodenext で拡張子を省いた | './math.js' と書く |
__dirname のエラーは、Node.js 22.23.4 では理由まで表示されました。
ReferenceError: __dirname is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/dev/shm/t285/lab/f2/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
// ESM での書き換え(Node.js 20.11 以降・22.16 以降は実験的機能ではない)
console.log(import.meta.dirname, import.meta.filename);
// 出力:/dev/shm/t285/lab/f2 /dev/shm/t285/lab/f2/d2.js
拡張子を省くと、正しい名前が提案されました。
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/dev/shm/t285/lab/f5/math' imported from /dev/shm/t285/lab/f5/app.js
Did you mean to import "./math.js"?
require is not defined・__dirname is not defined は、ブラウザで require を使った場合なども含めて、別記事「ReferenceError: X is not defined の原因と直し方」で詳しく扱います。パッケージそのものが見つからないエラー(Cannot find module 'express' など)は、関連記事「Node.js Cannot find moduleの原因と直し方」で扱っています。
このエラー(import を CommonJS で読んだ)
SyntaxError で1行目の import を指す
スタックに cjs/loader が出る
ESM として読ませる設定で直る
逆向きのエラー(require を ESM で使った)
ReferenceError で require・__dirname を指す
URL が file:/// で始まる
import・import.meta に書き換えて直る
OS・Node.jsの版・実行環境別の違い
この節では、Windows・Mac・Linux、Node.js の版、よく使うツールごとに、このエラーの出方が変わるかが分かります。
OS でこのエラーの出方は変わりません。変わるのは Node.js の版です。ESM と CommonJS の判定は Node.js の中で行われるため、Windows・Mac・Linux のどれでも同じ規則です。環境変数の付け方はシェルで違い、Jest 公式ドキュメントは Windows 向けに cross-env を案内しています。
版による違いは、Node.js 公式ドキュメントの履歴で確かめました。
- 1v20.10.0・v21.1.0:構文の自動判定がフラグ付きで追加この時点では既定で無効
- 2v20.11.0・v21.2.0:import.meta.dirname・filename が追加v22.16.0・v24.0.0 で実験的機能ではなくなった
- 3v22.7.0・v20.19.0:構文の自動判定が既定で有効"type" 無しの .js に import があっても動く
- 4v22.12.0・v20.19.0:require() で ESM を読める(フラグ不要)v22.13.0 で警告も出なくなった
- 5v22.18.0・v23.6.0:.ts の型ストリップが既定で有効.ts も "type" で ESM か CommonJS かが決まる
Jest 公式ドキュメントでは、Jest が CommonJS のコードからの require() による ESM 読み込みに対応するのは Node.js v24.9 以降と書かれています。手元の Node.js 22.23.4 では、前の節のとおり Babel などの変換が要りました。
CI と手元で結果が違うときは、CI の設定ファイルで指定している Node.js の版と、手元の node -v を並べて見ます。手元の版をそろえる道具は、関連記事「Node.js バージョン管理|nvm・fnm・mise・Volta比較と実測」で扱っています。
よくあるエラー/つまずき一覧(原文つき)
この節では、このエラーまわりで手元で出た原文と、その原因・直し方を1つの表で引けます。
| 表示された原文 | 場面 | 原因 | 直し方 |
|---|---|---|---|
| SyntaxError: Cannot use import statement outside a module | node app.js | "type": "commonjs"・.cjs | "type": "module"・.mjs |
| Warning: Failed to load the ES module … Make sure to set "type": "module" | node(エラーの直前) | 同上 | 表示どおりに直す |
| [MODULE_TYPELESS_PACKAGE_JSON] Warning: … Reparsing as ES module | node(動くが警告) | "type" が無い | "type": "module" を書く |
| Uncaught SyntaxError: Cannot use import statement outside a module | Chrome のコンソール | type="module" が無い | script に type="module" |
| Jest encountered an unexpected token … Cannot use import statement | Jest 29 | 変換していない | babel.config.js |
| Must use import to load ES Module | Jest 30 | 変換していない・node_modules の ESM | Babel・transformIgnorePatterns |
| TypeError: Unknown file extension ".ts" | ts-node | ESM の .ts を読めない | tsx・module を CommonJS 系に |
| Access to script … blocked by CORS policy(origin 'null') | file:// で開いた | module は file:// で読めない | ローカルサーバーで開く |
- 1^^^^^^ が指すファイルを見る自分のコード・node_modules・設定ファイルのどれかで、直す場所が変わる
- 2node -v と npm pkg get type を控える版と "type" の2つで、Node.js の原因はほぼ絞れる
- 3直したら require・__dirname を探すESM にそろえた直後のエラーの多くは、この2つが原因だった
- 4CI と手元の Node.js の版をそろえる自動判定の有無で、同じコードが動いたり止まったりする
判断表:どの直し方を選ぶか
この節では、自分の状況に合う直し方を1つ選べます。
迷ったら「プロジェクト全体を ESM にしてよいか」で分けます。よいなら "type": "module"、そうでないなら .mjs か import() です。
| 状況 | 選ぶ直し方 | 理由 |
|---|---|---|
| 新しく作る Node.js のプロジェクト | "type": "module" | 1行で済み、自動判定の読み直しも無くなる |
| require を多く使う既存のプロジェクト | import を書くファイルだけ .mjs | 他のファイルに影響しない |
| CommonJS のまま ESM だけのパッケージを使う | import()(Node.js 22.12 以降なら require も可) | 書き換えが最小で済む |
| HTML から読み込むファイル | script に type="module" | ブラウザではこれ以外に方法がない |
| Jest のテスト(モックを多く使う) | babel.config.js | jest.mock がそのまま使える |
| Jest で node_modules の中で止まる | transformIgnorePatterns | 必要なパッケージだけ変換できる |
| TypeScript を tsc でビルドして node で動かす | module: nodenext | "type" に合わせて出力が決まる |
| TypeScript をビルドせずに動かしたい | tsx | 手元では設定を変えずに動いた |
よくある質問
この節では、このエラーについてよく調べられる疑問に、手元の結果をもとに短く答えます。
Q. Cannot use import statement outside a module とはどういう意味ですか? A. 「モジュール(ES モジュール)の外では import 文を使えない」という意味です。import を書いたファイルが CommonJS や普通のスクリプトとして読み込まれたときに出ます。
Q. package.json に "type": "module" を書けば必ず直りますか? A. node で .js を実行して出た場合は、手元ではこれで直りました。.cjs・ブラウザ・Jest・tsc の出力には効かず、require や __dirname を使うファイルは別のエラーになります。
Q. "type" を書いていないのに、手元では動いて CI でだけエラーになります。 A. Node.js の版の違いが考えられます。v22.7.0・v20.19.0 以降は "type" が無いと構文を自動判定して ESM として読み直すため動きますが、それより前の版ではエラーになります。"type" を明示すると、版によらず同じ結果になります。
Q. .mjs と "type": "module" はどちらがよいですか? A. プロジェクト全体を ESM にするなら "type": "module"、一部のファイルだけ ESM にしたいなら .mjs です。逆に "type": "module" の中で CommonJS のファイルを残すときは .cjs にします。
Q. ブラウザで type="module" を付けたら、今度は CORS のエラーが出ました。 A. HTML ファイルを直接(file://)開いていないかを確かめます。module のスクリプトは file:// では読み込めず、手元でも origin 'null' の CORS エラーになりました。ローカルサーバーを立てて http:// で開くと動きます。
Q. Jest 30 では「Cannot use import statement」が出ません。別の問題ですか? A. 同じ問題です。手元の Jest 30.5.2 では「Must use import to load ES Module」と表示され、Jest 29.7.0 では「Cannot use import statement outside a module」でした。直し方(Babel・transformIgnorePatterns・ESM モード)も同じです。
Q. 自分のテストは通るのに、node_modules の中で止まります。 A. import したパッケージが ESM だけで配布されていて、Jest が node_modules を変換していないためです。transformIgnorePatterns でそのパッケージだけ変換の対象に戻すと、手元では通りました。
Q. TypeScript の tsconfig は module を何にすればよいですか? A. Node.js で動かすなら nodenext です。TypeScript 公式ハンドブックも Node.js には node16・node18・nodenext を勧めています。バンドラー(Vite など)を通すフロントエンドなら esnext(または preserve)と moduleResolution: bundler の組み合わせです。
Q. ts-node を実行しても何も表示されません。 A. 手元の ts-node 10.9.2(Node.js 22.23.4・TypeScript 6.0.3)では、module: ESNext と "type": "commonjs" の組み合わせで、エラーも出力も出ずに終わりました。module を CommonJS 系にすると動き、tsx は設定を変えずに動きました。
まとめ
この節では、記事の要点を5つにまとめます。
- 原因は、import を書いたファイルが ESM ではなく CommonJS(普通のスクリプト)として読まれたこと。エラーの ^^^^^^ と cjs/loader がその証拠です。
- Node.js は "type": "module" か .mjs、CommonJS のままなら import()。Node.js 22.12 以降は require() でも ESM を読めます。
- ブラウザは script に type="module"。直したあとは file:// の CORS・MIME タイプ・パッケージ名の解決に注意します。
- Jest は babel.config.js か transformIgnorePatterns、または ESM モード。Jest 30 では「Must use import to load ES Module」と表示が変わりました。
- TypeScript は module を nodenext に。"type" を明示すると、Node.js の版や自動判定によらず同じ結果になります。
参考・出典
確認日はいずれも 2026年11月5日です。
- Node.js「Modules: Packages」Determining module system・Syntax detection・"type":https://nodejs.org/api/packages.html
- Node.js「Modules: CommonJS modules」Loading ECMAScript modules using require():https://nodejs.org/api/modules.html
- Node.js「Modules: ECMAScript modules」import.meta.dirname・Mandatory file extensions:https://nodejs.org/api/esm.html
- Node.js「Modules: TypeScript」Type stripping:https://nodejs.org/api/typescript.html
- MDN「SyntaxError: import declarations may only appear at top level of a module」:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/import_decl_module_top_level
- Jest「ECMAScript Modules」(30.5):https://jestjs.io/docs/ecmascript-modules
- Jest「Configuring Jest」transformIgnorePatterns:https://jestjs.io/docs/configuration#transformignorepatterns-arraystring
- Jest「Code Transformation」:https://jestjs.io/docs/code-transformation
- TypeScript「Modules - Reference」module・moduleResolution:https://www.typescriptlang.org/docs/handbook/modules/reference.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cannot use import statementの原因と直し方|Jest・TS」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。