4
Help us understand the problem. What are the problem?

posted at

updated at

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コマンドのラッパーなんでしょうか?

ターミナル
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

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

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

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

image.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

プロジェクト全体をESModule化する

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

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

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

まず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.jsontypeを入力している際、VSCodeで以下の補足が出てきました。
拙いかつ意訳ですが、こんな感じ。
つまりやっぱりさっきまではCommonJSとして実行されていたような気がしますね。

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

image.png

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

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

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

image.png

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

image.png

ここまでを整理するとおそらくこのような流れだと考えています。

  • package.jsontype: moduleを設定することで、Node.jsはESMをネイティブでサポートしている。

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

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

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

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

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

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

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

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

image.png

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

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

おわりに

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

参考文献

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
4
Help us understand the problem. What are the problem?