Node.js の import と require の違いと ES モジュールの使い方

JavaScript で外部モジュールや自作のモジュールを読み込む際に使用する import と require の違いやエラーの対応について纏めています。

やりたいこと

JavaScript で外部モジュールや自作のモジュールを読み込む際に使用する import と require についてその違いや、現時点でモジュールシステムがダブルスタンダードになっていることにより、発生しがちなエラー等について纏めておきたいと思います。

なぜモジュールシステムが 2 つ存在するか

少し長くなりますが JavaScript の歴史を追いながら、なぜ Node.js におけるモジュールシステムがダブルスタンダードになったかを見てみたいと思います。

JavaScript の誕生と標準仕様策定

JavaScript は以下の通り Netscape Communications によって開発され、後に Ecma International によって ECMA-262 として標準化されるようになりました。

JavaScript はネットスケープコミュニケーションズのブレンダン・アイクによって、1995 年 5 月に 10 日間で開発された。(中略) 1997 年、通信に関する標準を策定する国際団体 Ecma インターナショナルによって JavaScript の中核的な仕様が ECMAScript として標準化され、多くのウェブブラウザで利用できるようになった。
- JavaScript - Wikipedia

ECMA-262 初版の時点で、後に仕様策定され今日 ECMAScript モジュール (ES Module) と呼ばれる import、 export や default は将来的な予約語とされており、当初よりモジュールシステムについて仕様策定する構想があったことがうかがえます。

FutureReservedWord :: one of case debugger export super catch default extends switch class do finally throw const enum import try
- ECMA-262, 1st edition

1999 年に第 3 版が策定された後、第 4 版が破棄されたことにより第 5 版の策定は 2009 年までかかります。ただ第 5 版の時点でもモジュールシステムについては未策定のままでした。

当時のオンブラウザの JavaScript のモジュール分けは HTML 上で必要なモジュールを <script> タグで読み込んで使用することしかできませんでした。ただ <script> タグで読み込むとグローバルに展開されるため名前空間の衝突があったり、この時点で JavaScript の変数宣言が再宣言可能な var しかなかったことで意図せず上書きされたりと到底モジュールと言えた代物ではありませんでした。

CommonJS プロジェクトによるモジュール仕様の策定

さて一方で 2009 年に Kevin Dangoor により Web ブラウザ以外の実行環境における JavaScript 仕様を策定するプロジェクト CommonJS が立ち上げられます。CommonJS プロジェクト立上げを宣言した Kevin Dangoor のブログ記事の中でも他のモジュールを取り込む標準的な方法が言語として定められていないことを問題点の一つとして挙げています。

JavaScript needs a standard way to include other modules and for those modules to live in discreet namespaces. There are easy ways to do namespaces, but there’s no standard programmatic way to load a module (once!). This is really important, because server side apps can include a lot of code and will likely mix and match parts that meet those standard interfaces.
- What Server Side JavaScript needs

CommonJS のモジュールシステムの仕様は Modules/1.1.1 - CommonJS Spec Wiki に定義があり、2013 年にリリースされた Node.js v0.10 のモジュールシステムは CommonJS のモジュールシステムを実装しています。(Modules Node.js v0.10.48 Manual & Documentation)

これが今日 CommonJS モジュールと呼ばれている require、exports によるモジュールシステムです。

CommonJS モジュールはブラウザでは使えませんが、WebPack のようなバンドラーの登場により、ブラウザ向け JavaScript の開発においても CommonJS モジュールベースで開発するスタイルが流行り始めました。開発時は CommonJS モジュールによるモジュール分割の恩恵を受けつつ、デプロイ時はバンドラーで 1 ファイルにモジュールを纏め、ブラウザで実行可能とするものです。

標準仕様でのモジュール仕様策定

そこから遅れること 2015 年に ECMA-262 第 6 版が策定され、以下の通り本文の導入部分にも記載されていますが大幅に仕様が追加され、ついにモジュールシステムの仕様が策定されました。

The sixth edition is the most extensive update to ECMAScript since the publication of the first edition in 1997.
- ECMAScript 2015 Language Specification - ECMA-262 6th Edition

この仕様策定を受け、2017~2020 年にかけて主要ブラウザで ES Module の機能実装がなされます。また Node.js においても 2017 年リリースの v8 に実験的機能として ES Module が追加されます。ECMAScript Modules | Node.js v8.17.0 Documentation

さらに 2019 年リリースの Node.js v12 で正式実装にされ、Node.js における CommonJS モジュール と ES Module のモジュールシステムダブルスタンダードの状態に至ります。(Modules: ECMAScript modules | Node.js v12.22.12 Documentation)

上記の通り ES Module はブラウザでも実装されているため、ついに Webpack のようなサードパーティツールに頼らずともネイティブにモジュールベースでブラウザ向け JavaScript の開発が可能となりました。(可能になったというだけで Webpack や Rollup といったバンドラーを使った開発が実態にはなっていると思います)

どちらを使うべきか

Node.js においては現時点 (v22.6.0) で CommonJS モジュールがデフォルトとなっています。デフォルトという言い方が曖昧なので具体的には package.json がないあるいは package.json の type フィールドがない場合、.js ファイルは CommonJS モジュールベースとして扱われることをデフォルトとここでは表現しています。

If the nearest parent package.json lacks a "type" field, or contains "type": "commonjs", .js files are treated as CommonJS. If the volume root is reached and no package.json is found, .js files are treated as CommonJS.
- Modules: Packages | Node.js v22.6.0 Documentation

これは長い間 CommonJS モジュールベースで Node.js のエコシステムが築かれてきたため、モジュールシステムのデフォルトの変更は影響が大きいためと考えられます。

ただ最近では既存の npm パッケージにおいても ES モジュール 対応が進んできており、CommonJS モジュールベースでないと困るケースは減ってきているように思います。(主観です)

何より ES モジュールは JavaScript 標準仕様で策定されているもので、今後 ES モジュールへの統一はさらに進んでいくものと想像されます。

また Node.js はブラウザ以外の JavaScript ランタイムとして広く使われた最古のものの一つで、前項で紹介したような歴史的背景でモジュールシステムがダブルスタンダードになっていますが、比較的新しいブラウザ以外の JavaScript ランタイムである Deno では最初から ES モジュールをベースにしています。

Deno by default standardizes the way modules are imported in both JavaScript and TypeScript using the ECMAScript module standard.
- ECMAScript Modules in Deno

以上のことを踏まえ、今後は ES モジュールを使っていくことがデファクトであると考えます。

Node.js で ES モジュールを使う

以下のように helloworkd 関数を名前付きエクスポートする module.js を作成します。

module.js
export const helloWorld = () => {
  console.log('Hello World');
}

名前付きエクスポート:
export キーワードの後には、let, const, var 宣言や、関数、クラス宣言を使用することができます。また、export { name1, name2 } 構文を使用すると、他の場所で宣言された名前のリストをエクスポートすることができます。
- export - JavaScript | MDN

次に先ほどの module.js から helloworld 関数を名前付きインポートして実行する index.js を作成します。

index.js
import { helloWorld } from "./module.js";
helloWorld();

では作成した index.js を早速実行してみます。

node index.js

すると以下のようなエラーが発生します。

SyntaxError: Cannot use import statement outside a module

前項でも触れましたがこれは Node.js のモジュールシステムは CommonJS モジュールがデフォルトになっているためです。

Node.js で ES モジュールを使用するには以下の通り拡張子を .mjs にするか、package.json の type フィールドに module を設定する必要があります。あるいは Node.js コマンドラインオプションの --input-type で module を指定するか、--experimental-default-type で module を指定する必要があります。

Authors can tell Node.js to interpret JavaScript as an ES module via the .mjs file extension, the package.json “type” field with a value "module", the --input-type flag with a value of "module", or the --experimental-default-type flag with a value of "module". These are explicit markers of code being intended to run as an ES module.
- Modules: ECMAScript modules | Node.js v22.6.0 Documentation

それでは index.js を index.mjs、module.js を module.mjs と拡張子を .mjs にそれぞれ変更します。実行するとエラーは出なくなります。

node index.mjs
Hello

ES モジュールで import する側も export する側も拡張子を .mjs にする必要があります。module.js の拡張子を .mjs にしなかった場合以下のエラーが発生します。

SyntaxError: Named export 'helloWorld' not found. The requested module './module.js' is a CommonJS module, which may not support all module.exports as named exports.

すべて ES モジュールを使っていて CommonJS モジュールの混在がない場合は、 package.json で "type"="module" を指定する方が簡単です。この場合、拡張子は js のままでも ES モジュールとして扱われます。

package.json
{
  "type": "module"
}

他にコマンドラインオプション --experimental-default-type に "module" を指定することで、拡張子は js のままでも ES モジュールとして扱われます。

node --experimental-default-type "module" index.js

コマンドラインオプション --input-type に "module" を指定する方法もあるようですが、REPL (Read-Eval-Print Loop) では使えないオプションなので試していません。

--input-type=type
The REPL does not support this option.
- Command-line API | Node.js v22.6.0 Documentation

TypeScript で Node.js 向け開発 (tsconfig.json の compilerOptions.module が node16 か nodenext になっている) をする場合も基本的には同様で、package.json で "type": "module" が指定されているか、拡張しが mts になっている場合、コンパイル後に ES モジュールベースで生成されます。

Input file name Contents Output file name Module kind Reason
/package.json {}
/main.mts /main.mjs ESM File extension
/utils.cts /utils.cjs CJS File extension
/example.ts /example.js CJS No “type”: “module” in package.json

- TypeScript: Documentation - Modules - Theory

ブラウザで ES モジュールを使う

ブラウザでの ES モジュールの使い方も紹介しておきます。前項で作成した module.js を使い回して、以下のように HTML 内の <script> タグ内でインポートしてみます。

<script>
  import { helloWorld } from "./module.js";
  console.log(helloWorld());
</script>

上記 HTML を HTTP サーバにホストしてブラウザでアクセスするとコンソールに以下のエラーが出ます。

Uncaught SyntaxError: Cannot use import statement outside a module

ブラウザベースで ES モジュールを使用するには以下の通り、<script> タグの type 属性に module を指定する必要があります。ブラウザベースで ES モジュールを使用するにはこれが唯一の方法で、Node.js のように拡張子を .mjs にすると ES モジュールとして扱ってくれるとかはありません。

ソースファイルの中で import 宣言を使用するためには、ランタイムがそのファイルをモジュールと見なす必要があります。HTML では、<script> タグに type="module" を加えることがこれに相当します。
import - JavaScript | MDN

<script> タグの type 属性に module を指定するとエラーはなくなり、コンソールに Hello と表示されます。

<script type="module">
  import { helloWorld } from "./module.js";
  console.log(helloWorld());
</script>

なお、HTML を HTTP サーバーでホストせずに file プロトコルで開くと、CORS に引っかかるので必ず HTTP でアクセスするようにしてください。

Access to script at 'file:///path/to/module.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome-untrusted, https, edge.

JSON ファイルのインポート

CommonJS モジュールの便利だったところとして JSON ファイルを JavaScript オブジェクトとしてインポートできる機能がありました。

// Importing a JSON file:
const jsonData = require('./path/filename.json');

- Modules: CommonJS modules | Node.js v22.6.0 Documentation

これ地味に便利なのですが、ES モジュールでは現時点ではこのような仕様はありません。ただまさに仕様策定中で Ecma TC39 (Technical Committee 39) プロセスの Stage 3 にいるので、数年後には以下のような形で正式に使えるようになると思われます。(The TC39 Process)

Developers will then be able to import a JSON module as follows:

import json from "./foo.json" with { type: "json" };
import("foo.json", { with: { type: "json" } });

- GitHub - tc39/proposal-json-modules: Proposal to import JSON files as modules

なお、Node.js では実験的機能として現時点の仕様に基づいて既にこの JSON モジュールは実装されています。

適当な JSON を用意します。

data.json
{
  "data": "Hello"
}

JSON モジュールでインポートしてみます。

import data from './data.json' with { type: 'json' };
console.log(data.data);

実行すると実験的機能なのでワーニングが出ますが、JSON を JavaScript オブジェクトとして扱えていることがわかります。

node index.mjs
Hello
(node:33604) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

JSON モジュールは 2024 年末にサポート中の各 LTS (v22.12.0, v20.18.3, v18.20.5) のリリースで実験的機能がはずれ正式に実装されました。以降のバージョンでは上記ワーニングは出ません。

なお、TypeScript を使用している場合、Cannot find module './data.json'. Consider using '--resolveJsonModule' to import module with '.json' extension. ts(2732) というエラーが出ます。これは tsconfig.json の compilerOptions.resolveJsonModule を true にすると解消されます。

{
  "compilerOptions": {
    "resolveJsonModule": true
  }
}

ちなみに多くのブラウザでもすでに実装されており (JavaScript statement: import: Import attributes (with syntax): with {type: 'json'} | Can I use… Support tables for HTML5, CSS3, etc)、通常の JavaScript モジュールと同様 type="module" を付けた <script> タグ内で JSON をインポートできます。

<script type="module">
import data from './data.json' with { type: 'json' };
console.log(data.data);
</script>

あとがき

Node.js に 2 種類のモジュールシステムが存在する経緯と ES モジュールの使い方について紹介しました。今後は ES モジュールがより加速していくと想像されるのでモジュールシステムの選択は慎重に行ったほうがよさそうです。