2022年度のTypeScript製ES Modulesレシピ

## はじめに

この記事は、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

::: note warn
ES Modulesに関わる環境は頻繁に更新されます。この記事は2022/10/15時点での環境設定をまとめたものです。
:::

node.jsのメジャーバージョンが更新された場合、この記事のレシピは適用できない可能性があります。お手元のnode.jsが20以降の場合はご注意ください。

## tsconfig.json

ES Modules対応のJavaScriptを出力するよう、TypeScriptを設定します。

https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping

TypeScriptのWikiに、それぞれのnode.jsバージョンに合わせた推奨設定がまとめられています。

https://github.com/tsconfig/bases/

また`@tsconfig/recommended`というパッケージにも、目的とする環境に合わせた推奨設定がまとめられています。[ES Modulesをターゲットとした推奨設定](https://github.com/tsconfig/bases/blob/main/bases/esm.json)もあります。

これらのサンプルをまとめると、ES Modules開発のためのtsconfig.jsonの設定は以下のようになります。

_▼tsconfig.json_

```jsonc
{
  "compilerOptions": {
    //必須の設定
    "lib": ["ES2021"],
    "module": "ES2022",
    "target": "ES2021",

    //オプション
    "declaration": true
  }
}
```

[declaration](https://www.typescriptlang.org/tsconfig#declaration)は型定義ファイルを出力するか否かの指定です。必須ではありませんが、作業効率が向上するので出力しましょう。

## package.json

モジュールがES Modules対応であることをpackage.jsonで明示します。

_▼package.json_

```jsonc
{
  "name": "あなたのパッケージの名前",
  "type": "module",
  "exports": {
    ".": {
      "import": {
          "types": "./esm/index.d.ts",
          "default": "./esm/index.js"
      },
  },
  "types": "./esm/index.d.ts" // 古いTypeScript用の型情報Fall-back
}
```

[type](https://nodejs.org/api/packages.html#type)フィールドは、拡張子`js`のファイルをES Modulesファイル（.mjs）と扱うか、CommonJSファイル（.cjs）ファイルとして扱うかを指定します。`module`を指定すると拡張子`js`のファイルは`mjs`ファイルとして扱われます。このフィールドを指定してもファイル名で`.cjs` `.mjs`を明示すれば、拡張子での指定が優先されます。

exportsの[types](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package)フィールドはTypeScriptの型定義ファイルを指定します。TypeScriptでES Modulesを開発するなら、作業効率が向上しますので指定しましょう。

:::note
`type`と`types`は見間違えそうになりますが、それぞれ別のフィールドです。ご注意ください。
:::

https://www.typescriptlang.org/docs/handbook/esm-node.html#packagejson-exports-imports-and-self-referencing

## TypeScript

### importと拡張子

TypeScriptのimportでは拡張子を省略します。TypeScriptのimportは

- 型情報が残っている`ts`ファイル
- トランスパイル済みの`js`ファイル
- 型情報のみの`d.ts`ファイル

を区別なく読み込むためです。この方針は[公式ドキュメント](https://www.typescriptlang.org/docs/handbook/modules.html#import)でも示されてきました。

しかし後発であるES Modulesのimportは、[拡張子を指定する](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%B8%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88)という方針を示しました。ここでES ModulesとTypeScriptの仕様に矛盾が生じました。

さまざまな解決方法が検討された結果、TypeScriptはES Modules対応のスクリプトを記述する時のみimport文に拡張子`js`を明記するようになりました。tscはimportを変換しません。この方針はこの[コメント](https://github.com/microsoft/TypeScript/issues/40878#issuecomment-702353715)で示されています。

_▼ファイル構成_

```shell
index.ts
ClassA.ts
tsconfig.json
```

このファイル構成でindex.tsからClassA.tsをimportする場合
_▼index.ts_

```ts
import { ClassA } from "./ClassA.js"
                                 ^^
```

このように存在しないトランスパイル済みファイル`ClassA.js`ファイルを指定しなくてはいけません。

:::note
この拡張子つきimport指定は、TypeScriptのGitHubリポジトリで現在もディスカッションが続いています。今後TypeScriptのバージョンアップで新たなオプションが追加されるかもしれません。ご注意ください。
:::

### jsonのimport

CommonJSにおいて、外部JSONファイルはrequire関数で簡単に読み込めました。しかし、ES Modulesにはrequire関数はありません。

```js
const data = require("./data.json");
```

node.js v16において、実験的機能として[JSON modules](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#json-modules)が実装されています。

```js
import packageConfig from './package.json' assert { type: 'json' };
```

この機能は[実験的](https://nodejs.org/dist/latest-v18.x/docs/api/documentation.html#stability-index)なため、将来のバージョンでも使い続けられるかわかりません。

```js
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const data = require("./data.json");
```

この問題を解決するのが[createRequire](https://nodejs.org/api/module.html#module_module_createrequire_filename)関数です。require関数を生成して、外部jsonファイルを読み込みます。

---

以上、ありがとうございました。