はじめに
普段Reactを触っているので、フロントエンドでのNode.jsは触ったことありますが、バックエンドは触ったことがありませんでした。
普通に環境構築すると、import文が使えず、require文になってしまい、とても不便だったので、ESModuleで環境構築を行なったメモを残したいと思います。
色々と誤りもあると思いますので、ご指摘いただけますと幸いです。
実行環境
macOS Monterey ver12.5.1
MacBookAir(M1, 2020)
パッケージのインストールを行う
まずNode.jsがインストールされていることを確認します。
Dockerで環境構築したい場合はコンテナの中に入ってコマンド実行してください。
node -v
v16.10.0
プロジェクトの初期化を行います。
色々質問されますが、とりあえず全てそのままで大丈夫です。
npm init
以下のようになっていることを確認してください。
{
"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
は以下のようになるはずです。
{
"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
を作成して、以下のコードを記述します。
import http from "http";
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end("hello world");
});
server.listen(8080);
ここで確認してほしいのは@types/node
をインストールしているので、型推論が効いていることです。
下の画像はhttp.createServer
にホバーした時の様子です。
では実行してみましょう。
ts-node
はローカルでインストールされていてグローバルにパスが通っていないので、ローカルのパッケージを実行するnpx
を使います。
yarn
でも似たような機能はあるらしいのですが、あまり使われていないようです。あまりこの辺りは調べられませんでした。
npx ts-node index.ts
ブラウザで開いてみましょう。表示されていますね。
では他のtsファイルからimportして実行するテストをしてみましょう。
まずsum.ts
を作成して以下のコードを記述します。
const sum = (a: number, b: number) => {
return a + b;
};
console.log(sum(1, 2));
importする前に実行してみましょう。
実行できているかと思います。
npx ts-node sum.ts
3
ではimportして使ってみましょう。
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
ブラウザで確認すると表示できているかと思います。
プロジェクト全体をESModule化する
これまで見てきた通りimport文を使えていますし問題なさそうに思います。
ここからは推測も混じってしまうのですが、おそらくts-node
が実行前にESModule
形式をCommonJS
形式に変換してくれて実行してくれている状態と思われます。
以下記事の言葉を借りるなら、現在は疑似ESModuleを使っている状態です。
現在の状態で何がマズいかと言うと、ESModule前提で作られたパッケージが読み込めない点にあるようです。以下参考記事ご覧ください。
そのためプロジェクト全体をESModule化したいと思います。
TypeScriptのDocsを参考にしながら行います。
まずtsconfig.json
を作成します。
npx tsc -init
公式に従って設定を行います。
{
"compilerOptions": {
"module": "nodenext",
}
}
{
"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とみなします。
さらにTypeScriptのDocsには以下の記載があります。
相対インポート パスには完全な拡張子が必要です (たとえば、import "./foo.js"の代わりに記述する必要がありますimport "./foo") 。
index.ts
を見に行くとこのようなエラーが出ています。
注意しないといけないのはimportするファイルの拡張子はjsを指定しないといけないという点です。
jsを指定したところエラーが解消されました。
ここまでを整理するとおそらくこのような流れだと考えています。
-
package.json
にtype: 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
ブラウザで動作確認出来ました。
しかし公式にも以下記述があるように実験的な機能であり、製品版では非推奨のようです。
tsc && node index.js
とかで実行するべきなのかも知れません。
ノードの ESM ローダー フックは実験的なものであり、変更される可能性があります。ts-node の ESM サポートは可能な限り安定していますが、新しいバージョンのノードで壊れる可能性がある API に依存しています。したがって、製品化にはお勧めできません。
おわりに
フロントエンドで慣れ親しんだimportでなおかつ型推論が欲しいだけなのですが、なかなか手間がかかりました。
Node.jsの作者がDenoというJSのランタイムを開発していますが、そちらではNode.jsの反省を活かして、TypeScriptやESModuleをネイティブにサポートしているようです。
とはいえ各種ライブラリの対応を考えると、Node.jsの覇権はしばらく続きそうですね。