ESMだのCJSだので詰まることが多すぎるので、いい加減情報をまとめる。
概要
ざっくりいうと、「modulesをどのように扱うか?」に係る2種の系統。
-
import,exportするほうがESM -
require,modules.exportするほうがCJS
ESM; ES Modules
importの方。
ECMA Script2015(ES6)で策定されたモジュール仕様。
V8などは当然こちらに準拠している。
CJS; CommonJS
requireのほう
立ち上げは2009なので、ESMより古参。
ServerSideJavascriptというコンセプトを出発点とするため、Node.jsではデフォルトである一方、ブラウザでは非対応となっている。
趨勢
メインストリームはESMである。
ことフロントエンド開発においてはそもそもブラウザがCJSに非対応であるため、必然的にESM環境になる。
しかし、Node.js環境上においても、npmの開発者であるIsaac氏自ら「CJSは時代遅れになりつつある」とコメントしている模様。
こうした状況を反映してか、最近のサードパーティモジュールの中にはCJSに対応していない(ESMのみ提供)ものが増えつつある。
……とされていたが、Node.js v22からそうでも無くなった模様。
CJSからESMをrequireで呼び出せるようになった。
ただし、Pure ESM packageという観点からは、importの使用を強く推奨されている。
Node.js + ESM + TypeScript
プロジェクト単位の手続き
一方、先述の通り、Node.jsにおいてはCJSをデフォルトとしている都合、Node.js環境を想定したプロジェクトをESM環境として扱うためにはボイラープレートな手続きが必要。
再掲・引用。
- Add
"type": "module"to your package.json.- Replace
"main": "index.js"with"exports": "./index.js"in your package.json.- Update the
"engines"field in package.json to Node.js 18:"node": ">=18".- Remove
'use strict';from all JavaScript files.- Replace all
require()/module.exportwithimport/export.- Use only full relative file paths for imports:
import x from '.';→import x from './index.js';.- If you have a TypeScript type definition (for example,
index.d.ts), update it to use ESM imports/exports.- Use the
node:protocol for Node.js built-in imports.
拡張子の明示と、TypeScriptでの都合
大きな違いとして、ESMでimportを行う際は拡張子を明示するのが原則。
import modules from "./modules.js"
ここまではまだ良いとして、TypeScriptでESM環境を想定する場合に留意点がある。
それは、.tsファイルをimportする場合でも、拡張子を.jsにする必要があるということ。
つまり、上記でimportするファイルがmodules.tsであろうとも、上のように.js拡張子で書かないといけない。
これは、tscのコンパイル方針に起因する。
tscはimport内の文字列の書き換えや読み替えを行わないという方針がTypeScript開発チームから示されている。
そのため、import内にはトランスパイル後の.js拡張子を記載する必要がある。
一応、このあたりはtscなどがIDE上でサポートしてくれるのでエラーになったりすることはない。気持ちは悪い。
一応、tsconfigの設定次第で.tsを扱えるとされているようだが……なんだか情報が錯綜している。