ESModuleかつTypeScriptでNode.jsの環境構築を行う

# はじめに
普段Reactを触っているので、フロントエンドでのNode.jsは触ったことありますが、バックエンドは触ったことがありませんでした。
普通に環境構築すると、import文が使えず、require文になってしまい、とても不便だったので、ESModuleで環境構築を行なったメモを残したいと思います。
色々と誤りもあると思いますので、ご指摘いただけますと幸いです。

# 実行環境
macOS Monterey ver12.5.1
MacBookAir(M1, 2020)

# パッケージのインストールを行う
まずNode.jsがインストールされていることを確認します。
Dockerで環境構築したい場合はコンテナの中に入ってコマンド実行してください。

```:ターミナル
node -v
v16.10.0
```

プロジェクトの初期化を行います。
色々質問されますが、とりあえず全てそのままで大丈夫です。

```:ターミナル
npm init
```

以下のようになっていることを確認してください。

```package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```

パッケージのインストールを行います。
今回`yarn`で行いますが、`npm`でももちろん大丈夫です。
まずは`typescript`をインストールします。
開発環境だけあれば良いので、`devDependencies`に追加されるようにします。

```:ターミナル
yarn add -D typescript
or
npm install --save-dev typescript
```

次にNode.jsの型定義をインストールします。
こちらも開発環境だけあれば良いです。
Node.js本体には型定義が入っていないので、これを入れないと型推論されません。

```:ターミナル
yarn add -D @types/node
or
npm install --save-dev @types/node
```

次に`ts-node`をインストールします。
Node.jsはデフォルトだとJavaScriptしか実行できず、TypeScriptを実行できません。
`ts-node`を使うことで、TypeScriptを直接実行できます。
公式リポジトリを見るに、`node`コマンドと`tsc`コマンドのラッパーなんでしょうか？

https://github.com/TypeStrong/ts-node#command-line

```:ターミナル
yarn add -D ts-node
or
npm install --save-dev ts-node
```

ここまで完了すれば`package.json`は以下のようになるはずです。

```package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "devDependencies": {
    "@types/node": "^18.7.14",
    "ts-node": "^10.9.1",
    "typescript": "^4.8.2"
  },
  "author": "",
  "license": "ISC"
}
```

# 実際に動かしてみる
簡単なサーバを立てるスクリプトを書いてみます。
`http://localhost:8080`にアクセスすると、`hello world`を返却するサーバです。
`index.ts`を作成して、以下のコードを記述します。

```index.ts
import http from "http";

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("hello world");
});

server.listen(8080);

```

ここで確認してほしいのは`@types/node`をインストールしているので、型推論が効いていることです。
下の画像は`http.createServer`にホバーした時の様子です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/158e4dd2-6c24-1dbf-eda3-b4c1cde8534d.png)

では実行してみましょう。
`ts-node`はローカルでインストールされていてグローバルにパスが通っていないので、ローカルのパッケージを実行する`npx`を使います。
`yarn`でも似たような機能はあるらしいのですが、あまり使われていないようです。あまりこの辺りは調べられませんでした。

```:ターミナル
npx ts-node index.ts
```

ブラウザで開いてみましょう。表示されていますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/b32f57d0-c042-e926-1b55-56b5d486849c.png)

では他のtsファイルからimportして実行するテストをしてみましょう。
まず`sum.ts`を作成して以下のコードを記述します。

```sum.ts
const sum = (a: number, b: number) => {
  return a + b;
};

console.log(sum(1, 2));
```

importする前に実行してみましょう。
実行できているかと思います。

```:ターミナル
npx ts-node sum.ts
3
```

ではimportして使ってみましょう。

```index.ts
import http from "http";
import { sum } from "./sum";

const server = http.createServer((req, res) => {
  const three = sum(1, 2);
  res.writeHead(200);
  res.end(three.toString());
});

server.listen(8080);
```

```:ターミナル
npx ts-node index.ts
```

ブラウザで確認すると表示できているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/06f3d73e-8e81-52f9-cc78-dc3cb21396b8.png)

# プロジェクト全体をESModule化する
これまで見てきた通りimport文を使えていますし問題なさそうに思います。
ここからは推測も混じってしまうのですが、おそらく`ts-node`が実行前に`ESModule`形式を`CommonJS`形式に変換してくれて実行してくれている状態と思われます。
以下記事の言葉を借りるなら、現在は疑似ESModuleを使っている状態です。

https://zenn.dev/teppeis/articles/2021-10-typescript-45-esm#%E7%94%A8%E8%AA%9E

現在の状態で何がマズいかと言うと、ESModule前提で作られたパッケージが読み込めない点にあるようです。以下参考記事ご覧ください。

https://www.memory-lovers.blog/entry/2022/05/31/110000

そのためプロジェクト全体をESModule化したいと思います。
TypeScriptのDocsを参考にしながら行います。

https://www.typescriptlang.org/docs/handbook/esm-node.html

まず`tsconfig.json`を作成します。

```:ターミナル
npx tsc -init
```

公式に従って設定を行います。

```tsconfig.json
{
    "compilerOptions": {
        "module": "nodenext",
    }
}
```

```package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^18.7.18",
    "ts-node": "^10.9.1",
    "typescript": "^4.8.3"
  },
  "type": "module"
}
```

ちなみに`package.json`の`type`を入力している際、VSCodeで以下の補足が出てきました。
拙いかつ意訳ですが、こんな感じ。
つまりやっぱりさっきまではCommonJSとして実行されていたような気がしますね。

> typeの値をmoduleにセットすると、パッケージは全てのjsファイルをESModuleだと判断するようになります。
> もしもtypeの値が省略されているか、commonjsの場合は全てのjsファイルをCommonJSとみなします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/985e0570-3f77-dead-901b-101c8c00f891.png)

さらにTypeScriptのDocsには以下の記載があります。

> 相対インポート パスには完全な拡張子が必要です (たとえば、import "./foo.js"の代わりに記述する必要がありますimport "./foo") 。

`index.ts`を見に行くとこのようなエラーが出ています。
注意しないといけないのはimportするファイルの拡張子はjsを指定しないといけないという点です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/a642d16f-a20b-4e50-3f32-89028822148e.png)

jsを指定したところエラーが解消されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/2d9340f3-77ae-5886-7b2f-98433278b3fd.png)

ここまでを整理するとおそらくこのような流れだと考えています。
- `package.json`に`type: module`を設定することで、Node.jsはESMをネイティブでサポートしている。

https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#modules-ecmascript-modules

- しかしNode.jsはtypescriptを直接実行できないため、tsconfig.jsonでESModuleにコンパイルするように設定している。

https://www.typescriptlang.org/docs/handbook/esm-node.html

動作確認してみましょう。

```:ターミナル
npx ts-node index.ts
```

エラーが出てしまいました。`ts-node`がESModule形式を読めないことに起因するようです。

```:ターミナル
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for 〜以下省略〜
```

ts-nodeの公式を参考にこのように変更します。

https://github.com/TypeStrong/ts-node#native-ecmascript-modules

```:ターミナル
node --loader ts-node/esm ./index.ts
```

ブラウザで動作確認出来ました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/973482/c2f41447-7b31-3077-5048-093bf18dc9f0.png)

しかし公式にも以下記述があるように実験的な機能であり、製品版では非推奨のようです。
`tsc && node index.js`とかで実行するべきなのかも知れません。

> ノードの ESM ローダー フックは実験的なものであり、変更される可能性があります。ts-node の ESM サポートは可能な限り安定していますが、新しいバージョンのノードで壊れる可能性がある API に依存しています。したがって、製品化にはお勧めできません。

# おわりに
フロントエンドで慣れ親しんだimportでなおかつ型推論が欲しいだけなのですが、なかなか手間がかかりました。
Node.jsの作者がDenoというJSのランタイムを開発していますが、そちらではNode.jsの反省を活かして、TypeScriptやESModuleをネイティブにサポートしているようです。
とはいえ各種ライブラリの対応を考えると、Node.jsの覇権はしばらく続きそうですね。

# 参考文献
- [yarnのaddに関するドキュメント](https://classic.yarnpkg.com/en/docs/cli/add)
- [ts-nodeのリポジトリ](https://github.com/TypeStrong/ts-node)
- [『npxってなんだ』](https://zenn.dev/844/articles/d06bfdbd2677a3)
- [『TypeScript 4.7 と Native Node.js ESM』](https://quramy.medium.com/typescript-4-7-%E3%81%A8-native-node-js-esm-189753a19ba8)
- [『TypeScript 4.5 以降で ESM 対応はどうなるのか？』](https://zenn.dev/teppeis/articles/2021-10-typescript-45-esm)
- [『TypeScriptのESMでハマる』](https://www.memory-lovers.blog/entry/2022/05/31/110000)
- [『ECMAScript Modules in Node.js』](https://www.typescriptlang.org/docs/handbook/esm-node.html)
- [Node.jsのesmに関するドキュメント](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html)