はじめに
この記事は、2022年におけるTypeScript製ES Modulesの作成方法を解説、共有するためのものです。
node.jsにおけるCommonJSの歴史は長く、それに関する記事が大量にあります。どの設定がCommonJSに関わり、どの設定がES Modulesに関わるのかを読み解くには労力がかかります。この記事ではPure ESM作成の最小限構成を通して、ES Modulesの設定を整理します。
想定する読者
- JavaScript、TypeScriptの開発経験者
- node.jsおよびnpmの基礎的な知識がある
- CommonJSのサポートは考えず、新規にPure ESMのパッケージを開発したい
想定する環境
- node.js v16.18.0 ~ v18.11.0
- TypeScript v4.8
ES Modulesに関わる環境は頻繁に更新されます。この記事は2022/10/15時点での環境設定をまとめたものです。
node.jsのメジャーバージョンが更新された場合、この記事のレシピは適用できない可能性があります。お手元のnode.jsが20以降の場合はご注意ください。
tsconfig.json
ES Modules対応のJavaScriptを出力するよう、TypeScriptを設定します。
TypeScriptのWikiに、それぞれのnode.jsバージョンに合わせた推奨設定がまとめられています。
また@tsconfig/recommended
というパッケージにも、目的とする環境に合わせた推奨設定がまとめられています。ES Modulesをターゲットとした推奨設定もあります。
これらのサンプルをまとめると、ES Modules開発のためのtsconfig.jsonの設定は以下のようになります。
▼tsconfig.json
{
"compilerOptions": {
//必須の設定
"lib": ["ES2021"],
"module": "ES2022",
"target": "ES2021",
//オプション
"declaration": true
}
}
declarationは型定義ファイルを出力するか否かの指定です。必須ではありませんが、作業効率が向上するので出力しましょう。
package.json
モジュールがES Modules対応であることをpackage.jsonで明示します。
▼package.json
{
"name": "あなたのパッケージの名前",
"type": "module",
"exports": {
".": {
"import": {
"types": "./esm/index.d.ts",
"default": "./esm/index.js"
},
},
"types": "./esm/index.d.ts" // 古いTypeScript用の型情報Fall-back
}
typeフィールドは、拡張子js
のファイルをES Modulesファイル(.mjs)と扱うか、CommonJSファイル(.cjs)ファイルとして扱うかを指定します。module
を指定すると拡張子js
のファイルはmjs
ファイルとして扱われます。このフィールドを指定してもファイル名で.cjs
.mjs
を明示すれば、拡張子での指定が優先されます。
exportsのtypesフィールドはTypeScriptの型定義ファイルを指定します。TypeScriptでES Modulesを開発するなら、作業効率が向上しますので指定しましょう。
type
とtypes
は見間違えそうになりますが、それぞれ別のフィールドです。ご注意ください。
TypeScript
importと拡張子
TypeScriptのimportでは拡張子を省略します。TypeScriptのimportは
- 型情報が残っている
ts
ファイル - トランスパイル済みの
js
ファイル - 型情報のみの
d.ts
ファイル
を区別なく読み込むためです。この方針は公式ドキュメントでも示されてきました。
しかし後発であるES Modulesのimportは、拡張子を指定するという方針を示しました。ここでES ModulesとTypeScriptの仕様に矛盾が生じました。
さまざまな解決方法が検討された結果、TypeScriptはES Modules対応のスクリプトを記述する時のみimport文に拡張子js
を明記するようになりました。tscはimportを変換しません。この方針はこのコメントで示されています。
▼ファイル構成
index.ts
ClassA.ts
tsconfig.json
このファイル構成でindex.tsからClassA.tsをimportする場合
▼index.ts
import { ClassA } from "./ClassA.js"
^^
このように存在しないトランスパイル済みファイルClassA.js
ファイルを指定しなくてはいけません。
この拡張子つきimport指定は、TypeScriptのGitHubリポジトリで現在もディスカッションが続いています。今後TypeScriptのバージョンアップで新たなオプションが追加されるかもしれません。ご注意ください。
jsonのimport
CommonJSにおいて、外部JSONファイルはrequire関数で簡単に読み込めました。しかし、ES Modulesにはrequire関数はありません。
const data = require("./data.json");
node.js v16において、実験的機能としてJSON modulesが実装されています。
import packageConfig from './package.json' assert { type: 'json' };
この機能は実験的なため、将来のバージョンでも使い続けられるかわかりません。
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const data = require("./data.json");
この問題を解決するのがcreateRequire関数です。require関数を生成して、外部jsonファイルを読み込みます。
以上、ありがとうございました。