JavaScriptの import / export とは、ファイル(モジュール)ごとに値を「公開する(export)」「受け取る(import)」ための標準の仕組みで、この方式を ESM(ECMAScript Modules)と呼びます。 Node.js が以前から使っている require / module.exports の方式は CommonJS(CJS)で、書き方だけでなく「いつ読み込まれるか」「this や __dirname があるか」「循環参照のときの挙動」も違います。

この記事では、すべての構文をコピペできる最小例と実行結果で一覧化し、ESM と CommonJS の対応表、Node 22 で .js / .mjs / .cjs と package.json の "type" をどう組み合わせると動く/動かないか(実際のエラー文つき)、ブラウザの <script type="module"> と importmap、パッケージを公開する側の "exports" の書き方までまとめました。コードは Node.js 22.22.2(require(esm) が既定で有効)と Chromium 141(Playwright 1.56) で実行しています。


import / export 構文の早見表

やりたいこと 公開する側(export) 受け取る側(import) 補足
名前付きで公開 export const PI = 3.14; / export function add() {} import { PI, add } from "./math.js"; 名前は一致させる。別名は add as plus
既定値として公開 export default function multiply() {} import multiply from "./math.js"; 受け取る名前は自由。1ファイル1つ
両方 上の2つを同じファイルに import multiply, { PI } from "./math.js"; default を先に書く
名前空間でまとめて受け取る ― import * as math from "./math.js"; math.PI / math.default
まとめて再公開 export * from "./math.js"; ― default は含まれない
名前を付けて再公開 export { add as sum } from "./math.js"; / export { default as multiply } from "./math.js"; ― 入口ファイル(index.js)向け
名前空間として再公開 export * as math from "./math.js"; import { math } from "./index.js"; ―
実行時に読み込む ― const { PI } = await import("./math.js"); Promise を返す。CommonJS でも使える
読み込んで実行するだけ ― import "./setup.js"; 値は受け取らない
自分のファイルの場所 ― import.meta.url / import.meta.filename / import.meta.dirname dirname は Node 20.11 以降
指定子を URL に解決 ― import.meta.resolve("./math.js") 文字列を同期で返す
JSON を読み込む ― import data from "./data.json" with { type: "json" }; with は必須
トップレベルで await export const v = await fetchSomething(); ― ESM のみ。CommonJS では不可
型だけを受け取る(TS) export type Order = {...} import { total, type Order } from "./util.ts"; 実行時に消える

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript import/export入門|ESMとCommonJSの違い」


検証環境

項目 内容
Node.js 22.22.2(process.features.require_module は true)
ブラウザ Chromium 141.0.7390.37(Playwright 1.56.0)
対応状況の確認 @mdn/browser-compat-data 8.1.3
公式ドキュメント nodejs.org の Modules: CommonJS modules / ECMAScript modules / Packages(v26.10.0 版と v22 系)

記事中の実行結果はこの環境の出力をそのまま載せています(ファイルパスの先頭は … で省略しています)。


構文ごとの最小例と実行結果

まず土台になるモジュールを1つ用意します。以降の例はすべてこのファイルを読み込みます。

// math.mjs(named export と default export を両方持つ)
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }

名前付き・default・両方を受け取る

// named.mjs
import { PI, add } from "./math.mjs";
console.log(PI, add(1, 2));
// default.mjs
import multiply from "./math.mjs";      // 名前は自由に付けられる
console.log(multiply(3, 4));
// both.mjs
import multiply, { PI, add as plus } from "./math.mjs";  // default は先に書く
console.log(multiply(2, PI), plus(1, 1));
$ node named.mjs
3.14 3
$ node default.mjs
12
$ node both.mjs
6.28 2

名前付き export は「名前で結びつく」ので {} の中の名前を一致させ、default export は「1ファイル1つの主役」なので受け取る側が名前を自由に決めます。

名前空間として受け取る

// ns.mjs
import * as math from "./math.mjs";
console.log(math.PI, math.add(1, 2), math.default(2, 3));
console.log(Object.keys(math));
3.14 3 6
[ 'PI', 'add', 'default' ]

default export は名前空間の中では default というキーになります。

再 export(入口ファイルを作る)

// reexport.mjs:複数モジュールをまとめて公開する「入口」ファイル
export * from "./math.mjs";                       // named をすべて再 export(default は含まれない)
export { default as multiply } from "./math.mjs"; // default に名前を付けて再 export
export { add as sum } from "./math.mjs";          // 別名で再 export
export * as math from "./math.mjs";               // 名前空間として再 export
// use_reexport.mjs
import * as api from "./reexport.mjs";
console.log(Object.keys(api));
console.log(api.sum(1, 2), api.multiply(2, 5), api.math.PI);
[ 'PI', 'add', 'math', 'multiply', 'sum' ]
3 10 3.14

export * from は default を含めません。 default も出したいときは export { default as 名前 } from を添えます。

動的 import()(実行時に決めて読み込む)

// dynamic.mjs
const { default: multiply, PI } = await import("./math.mjs");
console.log(multiply(2, PI));
console.log(typeof import("./math.mjs"));  // Promise
6.28
object

import() は Promise を返す関数のように使える構文で、条件分岐の中やイベントハンドラの中でも使えます。CommonJS ファイルの中でも使えるので、後述の「top-level await を含む ESM を CommonJS から読む」ときの解決策にもなります。

import.meta(自分のファイルの場所)

// meta.mjs
console.log(import.meta.url);
console.log(import.meta.filename);
console.log(import.meta.dirname);
console.log(import.meta.resolve("./math.mjs"));
file:///…/syntax/meta.mjs
/…/syntax/meta.mjs
/…/syntax
file:///…/syntax/math.mjs

import.meta.dirname / import.meta.filename は Node 20.11 / 21.2 で追加され、22.16 / 24.0 で安定版になりました(公式ドキュメントの History より)。__dirname の置き換えとしてそのまま使えます。ブラウザにはない Node 固有のプロパティなので、ブラウザと共有するコードでは new URL("./x", import.meta.url) のように import.meta.url を使います。

JSON を読み込む(import attributes)

// data.json → { "name": "sample", "version": "1.0.0", "tags": ["a", "b"] }
// json.mjs
import data from "./data.json" with { type: "json" };
console.log(data.name, data.tags);
const { default: data2 } = await import("./data.json", { with: { type: "json" } });
console.log(data2.version);
sample [ 'a', 'b' ]
1.0.0

with { type: "json" } は必須です(公式ドキュメント:The with { type: 'json' } syntax is mandatory)。Node 22.12 / 20.18.3 以降で安定版、ブラウザは Chrome 123・Safari 17.2・Firefox 138 以降で使えます。付け忘れたときのエラーは後述の「つまずき」にまとめました。

top-level await

// tla.mjs:async 関数で包まなくても await できる
const res = await new Promise((r) => setTimeout(() => r("ready"), 100));
console.log(res);
export const value = res;
ready

ESM ではファイルの直下で await が書けます(Node 14.8 以降)。ただし、このモジュールを CommonJS の require() で読むとエラーになります(後述)。

読み込んで実行するだけ(副作用 import)

// side_use.mjs
import "./sideeffect.mjs";   // 値は受け取らず、読み込んで実行するだけ
console.log("main");
setup.mjs が実行されました
main

polyfill の登録やグローバル設定など、「読み込むこと自体が目的」のときに使います。


ESM と CommonJS の違い(対応表)

観点 ESM(import / export) CommonJS(require / module.exports)
公開する export const a = 1; / export default x; module.exports = {...}; / exports.a = 1;
受け取る import { a } from "./m.js"; const { a } = require("./m.js");
読み込みのタイミング 静的:ファイル先頭で全部解決してから本体を実行 動的:require() を書いた場所で、その時点で実行
条件付きで読む await import() を使う if (...) require(...) が書ける
受け取る値 ライブバインディング(元の変数が変わると見える) コピー(export 時点の値)
this(トップレベル) undefined module.exports と同じオブジェクト
__dirname / __filename ない → import.meta.dirname / import.meta.filename ある
require / module / exports ない(createRequire で作れる) ある
strict モード 常に strict 指定しなければ非 strict
拡張子 相対パスでは必須(./a.js) 省略できる(./a → a.js / a/index.js)
ディレクトリ指定 不可(ERR_UNSUPPORTED_DIR_IMPORT) ./dir → dir/index.js
JSON with { type: "json" } が必要 require("./x.json") でそのまま
top-level await 使える 使えない
循環参照 未初期化の const に触ると ReferenceError、関数宣言は呼べる 途中まで埋まった module.exports(空のことが多い)が返る
NODE_PATH 効かない 効く
相手の方式を読む CJS を import できる(module.exports が default) ESM を require() できる(Node 22.12 以降、TLA なしのみ)
ファイルの判定 .mjs、または .js + "type": "module" .cjs、または .js + "type" なし/"commonjs"

this・__dirname・strict を実行で確認

// this.cjs
console.log("CJS this === module.exports:", this === module.exports);
console.log("CJS __dirname:", typeof __dirname, "require:", typeof require);
x = 1; // 非 strict なので宣言なし代入が通る
console.log("CJS 宣言なし代入 x =", x);
// this.mjs
console.log("ESM this:", this);
console.log("ESM __dirname:", typeof __dirname, "require:", typeof require);
try { y = 1; } catch (e) { console.log("ESM 宣言なし代入:", e.name, e.message); }
$ node this.cjs
CJS this === module.exports: true
CJS __dirname: string require: function
CJS 宣言なし代入 x = 1
$ node this.mjs
ESM this: undefined
ESM __dirname: undefined require: undefined
ESM 宣言なし代入: ReferenceError y is not defined

ライブバインディングとコピーの違い

// counter.mjs
export let count = 0;
export function increment() { count++; }
// live.mjs
import { count, increment } from "./counter.mjs";
console.log(count);
increment();
console.log(count); // 読み込み元でも増えて見える
// counter.cjs
let count = 0;
function increment() { count++; }
module.exports = { count, increment };
// copy.cjs
const { count, increment } = require("./counter.cjs");
const mod = require("./counter.cjs");
console.log(count);
increment();
console.log(count, mod.count); // export 時点の値がコピーされている
$ node live.mjs
0
1
$ node copy.cjs
0
0 0

ESM の import は「変数への参照」、CommonJS の require は「その時点の値のコピー」です。CommonJS で最新の値を見たいときは、値ではなく取得関数(getCount())を公開します。

読み込みのタイミング(静的 vs 動的)

// order.mjs
console.log("(1) これは import より後に実行される");
import { v } from "./log.mjs";  // 位置に関係なく先に読み込まれる(静的)
console.log("(3)", v);
// order.cjs
console.log("(1) 先に実行される");
const { v } = require("./log.cjs");  // 書いた場所で読み込む(動的)
console.log("(3)", v);
$ node order.mjs
(2) 依存モジュールが先に実行される
(1) これは import より後に実行される
(3) 1
$ node order.cjs
(1) 先に実行される
(2) require された時点で実行される
(3) 1

import はファイルのどこに書いても先頭に巻き上げられ、依存先が先に実行されます。 そのため ESM では「import の前に環境変数を設定する」ような書き方は効かず、設定は別モジュールに分けて先に import します。

循環参照のときの挙動

// a.cjs
console.log("a: 開始");
const b = require("./b.cjs");
console.log("a: b.done =", b.done);
module.exports = { done: true };

// b.cjs
console.log("b: 開始");
const a = require("./a.cjs");           // a はまだ途中 → その時点の module.exports(空)が返る
console.log("b: a.done =", a.done);
module.exports = { done: true };
$ node a.cjs
a: 開始
b: 開始
b: a.done = undefined
a: b.done = true
(node:9153) Warning: Accessing non-existent property 'done' of module exports inside circular dependency
// a.mjs
console.log("a: 開始");
import { bDone } from "./b.mjs";
console.log("a: bDone =", bDone);
export const aDone = true;
export function aFn() { return "aFn"; }

// b.mjs
console.log("b: 開始");
import { aDone, aFn } from "./a.mjs";
console.log("b: aFn() =", aFn());        // 関数宣言は巻き上げられるので呼べる
try { console.log(aDone); } catch (e) { console.log("b: aDone →", e.name + ":", e.message); }
export const bDone = true;
$ node a.mjs
b: 開始
b: aFn() = aFn
b: aDone → ReferenceError: Cannot access 'aDone' before initialization
a: 開始
a: bDone = true

CommonJS は「空のオブジェクト」が黙って返り、ESM は「未初期化」として ReferenceError になります。 ESM のほうが問題に早く気づけ、関数宣言なら循環していても呼べます。どちらの方式でも、循環は共通部分を第3のモジュールに切り出すと解消できます。


Node 22 で「どのファイルがどちらの方式で動くか」

判定ルール(公式ドキュメントの要約)

ファイル 最寄りの package.json の "type" 扱い
.mjs 何でも 常に ESM
.cjs 何でも 常に CommonJS
.js "module" ESM
.js "commonjs" CommonJS
.js なし(フィールドがない/package.json がない) まず CommonJS。import/export 構文があれば ESM として読み直す(構文検出)

"type" の既定値は "commonjs" です。公式ドキュメントは「すべて CommonJS のパッケージでも "type" を明記することを推奨」しています。

実際に動かした結果(Node 22.22.2)

import 構文の .js、require の .js、exports.x = の .js を、"type" の設定を変えて実行しました。

.js の中身 "type" なし(package.json なし) "type" なし(package.json あり) "type": "commonjs" "type": "module"
import 構文 動く 動く(警告あり※1) SyntaxError: Cannot use import statement outside a module 動く
require(...) 動く 動く 動く ReferenceError: require is not defined in ES module scope, you can use import instead
exports.x = ... 動く 動く 動く ReferenceError: exports is not defined in ES module scope

※1 の警告文(そのまま):

(node:9450) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///…/notype/esm-syntax.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 /…/notype/package.json.

"type": "module" 側のエラーには、直し方まで書かれています。

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/…/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

迷ったら「package.json に "type": "module" を書き、ESM で統一する。CommonJS のまま残したいファイルだけ .cjs にする」が最も見通しのよい構成です。

require(esm):CommonJS から ESM を require する

Node 公式ドキュメント(Modules: CommonJS modules の History)によると、require() で ESM を読む機能は v22.12.0 / v20.19.0 でフラグなしで有効になり、v22.13.0 で実験的警告も出なくなり、v25.4.0 で正式機能になりました。条件は「モジュールグラフに top-level await がないこと」です。

// lib.mjs
export const name = "esm-lib";
export default function hello() { return "hello from esm"; }

// req-esm.cjs
const lib = require("./lib.mjs");
console.log("require(esm) →", lib);
console.log("default:", lib.default(), "named:", lib.name);
$ node req-esm.cjs
require(esm) → [Module: null prototype] {
  __esModule: true,
  default: [Function: hello],
  name: 'esm-lib'
}
default: hello from esm named: esm-lib

戻り値は モジュール名前空間オブジェクトで、default export は .default に入り、default があるときは __esModule: true が付きます(トランスパイル済みコードとの互換のため)。require() の戻り値そのものを差し替えたいときは、ESM 側で export { Point as "module.exports" } と公開します(公式ドキュメント記載の方法)。

top-level await を含む ESM を require() すると次のエラーになります。

$ node req-tla.cjs
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 /…/req-tla.cjs
  Requiring /…/tla.mjs

この場合は import() に切り替えます。CommonJS の中でも使えます。

// import-tla.cjs
import("./tla.mjs").then((m) => console.log("import() →", m.v));
(async () => { const { v } = await import("./tla.mjs"); console.log("await import() →", v); })();
import() → 1
await import() → 1

--no-experimental-require-module を付けて機能を無効化すると、22.11 以前と同じ ERR_REQUIRE_ESM になります。古い Node で見かけるエラーはこれです。

$ node --no-experimental-require-module req-esm.cjs
Error [ERR_REQUIRE_ESM]: require() of ES Module /…/lib.mjs from /…/req-esm.cjs not supported.
Instead change the require of /…/lib.mjs to a dynamic import() which is available in all CommonJS modules.

ESM から CommonJS を import する(named import が取れる条件)

ESM から CommonJS を import すると、module.exports 全体が default になります。名前付き import は ソースの静的解析(cjs-module-lexer)で検出できた名前だけ使えます。5パターンを試しました。

// p1.cjs: exports.name = "p1"; exports.hello = () => "hi";
// p2.cjs: const name = "p2"; const hello = () => "hi"; module.exports = { name, hello };
// p3.cjs: module.exports = { name: "p3", hello: () => "hi" };
// p4.cjs: const obj = { name: "p4", hello: () => "hi" }; module.exports = obj;
// p5.cjs: module.exports = function greet() {}; module.exports.name2 = "p5";
// check.mjs
for (const f of ["./p1.cjs", "./p2.cjs", "./p3.cjs", "./p4.cjs", "./p5.cjs"]) {
  const ns = await import(f);
  console.log(f, "→ keys:", Object.keys(ns), "| ns.name =", ns.name);
}
./p1.cjs → keys: [ 'default', 'hello', 'name' ] | ns.name = p1
./p2.cjs → keys: [ 'default', 'hello', 'name' ] | ns.name = p2
./p3.cjs → keys: [ 'default' ] | ns.name = undefined
./p4.cjs → keys: [ 'default' ] | ns.name = undefined
./p5.cjs → keys: [ 'default', 'name2' ] | ns.name = undefined
CommonJS 側の書き方 名前付き import 理由
exports.name = ... できる 代入が静的に見える
module.exports = { name, hello }(短縮記法) できる 識別子のみのオブジェクトは検出される
module.exports = { name: "p3", ... }(値が式) できない 検出が止まる
module.exports = obj(変数を代入) できない 中身が追えない

検出できない名前を import { name } from "./p3.cjs" と書くと、実行前にエラーになります。

SyntaxError: Named export 'name' not found. The requested module './p3.cjs' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from './p3.cjs';
const { name } = pkg;

確実なのは、エラー文が示すとおり default で受け取って分割代入する方法です。 ESM の中で require を使いたいときは createRequire を使います。

// fix_p3.mjs
import pkg from "./p3.cjs";      // default に module.exports 全体が入る
const { name, hello } = pkg;
console.log(name, hello());
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
console.log(require("./p3.cjs").name);  // ESM の中で require を使う方法
p3 hi
p3

ブラウザで使う:script type="module" と importmap

ブラウザでは <script type="module"> で ESM が使えます。npm パッケージ名のような「裸の名前(bare specifier)」はそのままでは解決できないので、importmap で対応表を書きます。importmap は Chrome/Edge 89・Firefox 108・Safari 16.4 以降で使え、module script より前に置く必要があります。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- importmap は module script より前に置く -->
  <script type="importmap">
  {
    "imports": {
      "lit-like": "./modules/lit-like.js"
    }
  }
  </script>
  <script type="module" src="./modules/main.js"></script>
  <script nomodule>console.log("module 非対応ブラウザだけで動く");</script>
  <script>console.log("classic script:", typeof this, document.readyState);</script>
</head>
<body><p>ESM デモ</p></body>
</html>
// modules/main.js
import { name, area } from "./shapes.js";      // 相対パス+拡張子は必須
import { html } from "lit-like";                 // importmap で解決される「裸の名前」
console.log(`module: ${name}, area(2)=${area(2)}, ${html("ok")}`);
console.log("this in module:", this);            // undefined
console.log("document.readyState:", document.readyState); // 既定で defer 相当
const { name: n2 } = await import("./shapes.js");           // top-level await も使える
console.log("dynamic import:", n2);

Node の http で静的配信し、Playwright の Chromium で開いてコンソールを取得した結果です。

[log] classic script: object loading
[log] module: shapes, area(2)=13, html(ok)
[log] this in module: undefined
[log] document.readyState: interactive
[log] dynamic import: shapes

module script は自動で defer 扱い(HTML の解析が終わってから実行、readyState が interactive)で、this は undefined、top-level await も使えます。nomodule のスクリプトは ESM 対応ブラウザでは実行されません(出力に含まれていません)。

importmap なしで裸の名前を書いた場合と、拡張子を省略した場合のブラウザ側のエラーも取得しました。

[pageerror] Failed to resolve module specifier "lit-like". Relative references must start with either "/", "./", or "../".
[error] Failed to load resource: the server responded with a status of 404 (Not Found)
注意点 内容
配信の MIME タイプ .js は text/javascript で返す(file:// で直接開くと CORS の制約で読み込めない)
importmap の数 現在のブラウザは複数書けるが、古い版は1つだけ。module script より前に置く
src 属性には効かない importmap は import 文と import() の指定子だけに適用される
Worker importmap は Worker 内のモジュールには適用されない

パッケージを公開する側:"exports" / "main" / "module" の書き方

npm パッケージを ESM と CommonJS の両方から使えるようにする(デュアルパッケージ)には、package.json の "exports" に条件付きの入口を書きます。

{
  "name": "demo-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  }
}
フィールド 役割 備考
"exports" 入口ファイルと公開するサブパスの一覧。"main" より優先 書いたパス以外は外から読めなくなる(ERR_PACKAGE_PATH_NOT_EXPORTED)
"main" "exports" を解釈しない古い Node やツール向けの入口 "exports" と併記しておく
"module" バンドラー(webpack など)向けの ESM 入口 Node は解釈しない(公式ドキュメントの package.json フィールド一覧にない)
"type" .js の扱い("module" / "commonjs") 公開パッケージでも明記が推奨
"imports" パッケージ内部用の # で始まる別名 "#internal/*.js": "./src/internal/*.js"

条件の順序には意味があり、具体的なものから順に書きます(公式:conditions should be from most specific to least specific)。"types" を使うときは先頭に置きます。

ローカルの node_modules にこのパッケージを置き、ESM と CommonJS の両方から読みました。

// use.mjs
import hello, { format } from "demo-lib";
import { util } from "demo-lib/utils";
import * as mo from "mainonly";     // "main": "./index.cjs", "module": "./index.mjs" のパッケージ
console.log("ESM から:", format, hello(), "|", util, "|", mo.default.from);

// use.cjs
const lib = require("demo-lib");
const { util } = require("demo-lib/utils");
console.log("CJS から:", lib.format, lib.default(), "|", util, "|", require("mainonly").from);
try { require("demo-lib/dist/internal.js"); } catch (e) { console.log(e.code + ":", e.message.split("\n")[0]); }
$ node use.mjs
ESM から: esm hello (esm) | utils (esm) | main (index.cjs)
$ node use.cjs
CJS から: cjs hello (cjs) | utils (cjs) | main (index.cjs)
ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './dist/internal.js' is not defined by "exports" in /…/node_modules/demo-lib/package.json

import からは "import" 側、require からは "require" 側のファイルが選ばれ、"module" フィールドは ESM から読んでも無視されて "main" の CommonJS が使われることが確認できます。

二重評価(dual package hazard)と "module-sync"

"import" と "require" で別ファイルを配ると、1つのプロセスの中で 同じパッケージが2回評価されることがあります。状態やクラスが別物になる様子を確認しました。

// state-lib: "exports": { "import": "./index.js", "require": "./index.cjs" }(どちらも registry と Item を公開)
import { registry, Item } from "state-lib";          // ESM 側の実体
import { createRequire } from "node:module";
const cjs = createRequire(import.meta.url)("state-lib"); // CJS 側の実体(別ファイル)
registry.push("from esm");
console.log("ESM 側:", registry, "| CJS 側:", cjs.registry);
console.log("new Item() instanceof cjs.Item:", new Item() instanceof cjs.Item);
ESM 側: [ 'from esm' ] | CJS 側: []
new Item() instanceof cjs.Item: false

require(esm) が使える Node(22.12 以降)向けには、"module-sync" 条件で ESM 1本を両方に配る書き方があります(公式:import / import() / require() のどれで読まれても一致し、top-level await を含まない ESM を想定)。

{
  "name": "state-sync",
  "type": "module",
  "exports": {
    "node": { "module-sync": "./index.js", "default": "./dist.cjs" },
    "default": "./index.js"
  }
}
ESM 側: [ 'from esm' ] | CJS 側: [ 'from esm' ]
new Item() instanceof cjs.Item: true

"module-sync" を解釈しない古い Node は "default" の CommonJS 版に落ちるので、両方に対応できます。

公開方式 向いている場面 注意
ESM のみ("type": "module" + "exports": "./index.js") 利用者が Node 22.12 以降/バンドラー前提 古い Node の require() は ERR_REQUIRE_ESM
"import" / "require" で2種類配布 幅広い Node に対応したい 二重評価に注意(状態は持たせない)
"module-sync" + "default" 新しい Node では ESM 1本、古い Node は CJS top-level await を使わない

TypeScript の module / moduleResolution(要点だけ)

TypeScript 側の設定は tsconfig の記事で扱っているので、ここでは import/export に関わる点だけ表にします。

使い方 module moduleResolution 相対 import の拡張子
Vite・Next.js などバンドラーで動かす esnext / preserve bundler 省略できる
Node.js でそのまま動かす(ESM) nodenext 自動で nodenext .js を書く(出力後の拡張子)
Node.js で CommonJS として出力 commonjs node16 / nodenext + "type": "commonjs" 省略できる

Node 22.18 以降は型注釈を消して .ts を直接実行できます。このときは .ts 拡張子のまま import します。

// util.ts
export type Order = { id: number; qty: number };
export function total(orders: Order[]): number { return orders.reduce((s, o) => s + o.qty, 0); }

// main.ts
import { total, type Order } from "./util.ts";   // .ts 拡張子で書く
const orders: Order[] = [{ id: 1, qty: 2 }, { id: 2, qty: 3 }];
console.log(total(orders));
$ node main.ts
5

./util.js と書いた場合は ERR_MODULE_NOT_FOUND: Cannot find module '/…/util.js' になります(tsc でビルドして実行する構成とは逆なので、どちらの構成かを先に決めます)。


よくあるつまずきと直し方

症状(エラー文) 原因 直し方
SyntaxError: The requested module './math.js' does not provide an export named 'multiply' default export を { } で受け取っている import multiply from "./math.js"
SyntaxError: Named export 'name' not found. The requested module './p3.cjs' is a CommonJS module... CommonJS の named export が検出できない import pkg from で受けて分割代入
ns.name が undefined(エラーは出ない) import * as ns で CommonJS を読み、検出されない名前を参照 同上(ns.default.name)
ERR_MODULE_NOT_FOUND: Cannot find module '/…/helper' ... Did you mean to import "./helper.js"? 相対パスの拡張子を省略 .js を付ける
ERR_UNSUPPORTED_DIR_IMPORT: Directory import '/…/utils' is not supported ディレクトリを指定 ./utils/index.js まで書く
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///…/conf.json" needs an import attribute of "type: json" JSON に with がない with { type: "json" } を付ける
SyntaxError: Cannot use import statement outside a module .js が CommonJS 扱い "type": "module" を書くか .mjs にする
ReferenceError: require is not defined in ES module scope .js が ESM 扱い import に書き換えるか .cjs にする
ERR_REQUIRE_ASYNC_MODULE top-level await を含む ESM を require() await import() にする
ERR_REQUIRE_ESM Node 22.11 以前、または機能を無効化している Node を更新するか import() にする
ERR_PACKAGE_PATH_NOT_EXPORTED "exports" に書かれていないサブパスを指定 公開側の "exports" に追加する
ブラウザ:Failed to resolve module specifier "lit-like" 裸の名前に importmap がない importmap を書くか相対パスにする

直す前と後:default export を名前付きで受け取っていた

// 直す前
import { multiply } from "./math.js";
// SyntaxError: The requested module './math.js' does not provide an export named 'multiply'

// 直した後(default は名前を自由に付けて受け取る)
import multiply from "./math.js";

直す前と後:ESM で __dirname を使っていた

// 直す前
const configPath = path.join(__dirname, "config.json");
// ReferenceError: __dirname is not defined in ES module scope

// 直した後(Node 20.11 以降)
const configPath = path.join(import.meta.dirname, "config.json");
// ブラウザと共有するコードなら
const configUrl = new URL("./config.json", import.meta.url);

よくある質問

Q. 新しく作るプロジェクトは ESM と CommonJS のどちらにすべきですか? A. ESM が標準で、ブラウザ・Node・Deno・Bun のすべてで動きます。package.json に "type": "module" を書いて ESM に統一し、CommonJS のままにしたいファイルだけ .cjs にすると見通しがよくなります。Node 22.12 以降は require(esm) も使えるので、CommonJS 側からも ESM のライブラリを読めます。

Q. import と require を1つのファイルで混ぜて使えますか? A. ESM ファイルには require がありませんが、createRequire(import.meta.url) で作れます。CommonJS ファイルには import 文は書けませんが、import()(動的 import)は使えます。

Q. export default と名前付き export はどう使い分けますか? A. 「そのファイルの主役が1つ」(クラス1つ、コンポーネント1つ)なら default、複数のユーティリティを公開するなら名前付きが向きます。名前付きは補完と検索が効きやすく、export * from でまとめ直しやすいのが利点です。

Q. import.meta.dirname が使えない環境ではどうしますか? A. Node 20.11 より前では path.dirname(fileURLToPath(import.meta.url)) を使います。ブラウザには dirname がないので、共有コードでは new URL("./file", import.meta.url) で URL として扱います。


まとめ

  • export で公開し import で受け取る。名前付きは名前で結びつき、default は受け取る側が名前を決める
  • export * from は default を含まない。動的な読み込みは import()、JSON は with { type: "json" } が必須
  • ESM は静的(先頭で解決・ライブバインディング・常に strict・this は undefined)、CommonJS は動的(書いた場所で実行・値のコピー)
  • Node 22 では .mjs / .cjs が確定、.js は package.json の "type" で決まる。迷ったら "type": "module" で統一
  • require(esm) は Node 22.12 以降で使える。top-level await があるときだけ import() に切り替える
  • CommonJS からの named import は静的解析で検出できた名前だけ。確実なのは default で受けて分割代入
  • 公開パッケージは "exports" に "import" / "require"(または "module-sync")を書き、"module" フィールドは Node には効かない

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript import/export入門|ESMとCommonJSの違い」

株式会社RJCでは、モジュールの仕組みのような「毎日使うのに意外と深い」基礎を、実際に動かして確かめながらプロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?