Node.js Worker Threads: TypeScriptのワーカーを起動する方法 〜ts-node、ts-node-devに対応する方法〜

Node.jsのWorker Threadsは、本物のスレッドプログラミングができます。ワーカーの処理を記述したJavaScriptを与えて、ワーカーを起動するわけですが、TypeScriptのファイルを指定するにはどしたらいいのでしょうか？

## 本稿でわかること

* ts-nodeとWorker Threadsを組み合わせて、TypeScriptのワーカーを起動する方法
* ts-node-devでTypeScriptのワーカーを起動する方法

## 前提知識

本稿を理解するにあたっては、下記の技術についての基礎的な知識が必要です。

* Worker Threads
    * 本物のスレッドプログラミングができるNodeモジュール。
    * 概要と基礎的な使い方は、次の投稿をご覧ください。
        * [Node.js: CPU負荷で3秒かかっていた処理を「Worker Threads」で1秒に時短する - Qiita](https://qiita.com/suin/items/bce351c812603d413841)
* ts-node
    * TypeScriptのコンパイルとJavaScriptの実行をコマンド一つでできるツール。
        * `tsc && node dist/main.js`を一発でできるようにしたツール。
    * nodeコマンドの感覚でTypeScriptを実行できる。 例: `ts-node src/main.ts`
* ts-node-dev
    * ts-nodeとnode-devを組み合わせた開発ツール。
    * TypeScriptのコードに変更が加わると自動的にコンパイルし、プログラムを再起動してくれる。

## 解決したい課題: WorkerにTypeScriptを指定することはできない

JavaScriptでWorker Threadsを実行する方法は以下のような手順になります。

まず、ワーカー側の処理を実装したJavaScriptファイルを作ります:

```worker.js
console.log('Hello from worker')
```

次に、ワーカーを起動する処理を書くのですが、`Worker`コンストラクタでワーカーのファイル名を指定する必要があります:

```main.js
const {Worker} = require('worker_threads')
const worker = new Worker('./worker.js')
```

このmain.jsを実行すると、ワーカーが起動することが確認できます:

```
$ node main.js
Hello from worker
```

このコードをTypeScriptで書き直し、ts-nodeで同じように実行するとどうなるでしょうか？ やってみましょう。

まず、ワーカー側の実装をTSに移植します。内容はworker.jsと全く同じです:

```worker.ts
console.log('Hello from worker')
```

次に、main.jsをTSに移植します。大きな変更は、`worker.js`ではなく`worker.ts`を起動するように変える点です:

```main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.ts') // tsファイルを指定
```

このコードを、ts-nodeで起動してみます。すると、次のようなエラーが発生し、ワーカーが起動できないことが分かります。エラー内容は、「ワーカースクリプトの拡張子はjs, mjs, cjsじゃないとダメだよ」というものです。

```
$ ts-node src/main.ts
The worker script extension must be ".js", ".mjs", or ".cjs". Received ".ts"
```

このことから、**Worker Threadsでは直接TypeScriptファイルが指定できない**ことが分かったと思います。

## 解決策: いったんJavaScriptファイルを経由するようにする

Worker Threadsで起動できるコードはJavaScriptのみという制約があるので、直接TypeScriptのワーカーを起動するのはあきらめます。迂回手段として、まずJavaScriptのワーカーを起動し、その中でTypeScriptコードを`require`するようにします。

先述した失敗作TypeScriptコードを手直ししていきましょう。

まず、main.tsはworker.tsではなく、worker.jsを起動するように直します:

```main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.js') // jsを起動するように直す
```

次に、最終的な目的地である、TypeScriptワーカーのファイルを作ります。名前はworker.jsと区別できるようtsWorker.tsにしておきます:

```tsWorker.ts
console.log('tsWorker.ts started')
```

最後に、main.tsとtsWorker.tsを橋渡しする、worker.jsを実装します。worker.jsの重要な役割は、ts-nodeをregisterすることです。これにより、以降のコードではtsファイルをrequireして実行できるようになります:

```worker.js
console.log('worker.js started')
require('ts-node').register() // 重要
require(__dirname + '/tsWorker.ts')
```

このコードを実行してみましょう。

```
$ ts-node src/main.ts
worker.js started
tsWorker.ts started
```

出力結果から、まずworker.jsが実行され、次にtsWoekr.tsが読み込まれ実行されたことがわかると思います。

## ts-node-devでは、execArgvを空っぽにしてWorkerを起動する

これまでts-nodeでTypeScriptワーカーを起動する方法を説明してきましたが、類似のツールであるts-node-devでも同じ方法で対応できるのでしょうか？ 結論を言うと、そのままでは対応できません。

上のmain.tsをts-node-devで実行してみると分かりますが、worker.jsは起動するものの、worker.js内のrequireが動作せず、スレッドが終了してしまいます:

```
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
```

worker.jsで`execArgv`を確認すると、ワーカー側では不要なts-node-devのフックが渡ってきているのがわかります:

```worker.js
console.log('worker.js started')
console.log(process.execArgv)
// require('ts-node').register()
// require(__dirname + '/tsWorker.ts')
```

```text:実行結果
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
[
  '-r',
  '/var/folders/4l/mrmcxh3x40lbcpwxyz29ppcw0000gn/T/ts-node-dev-hook-1629690677650566.js'
]
```

解決策としては、main.tsの`new Worker`のオプションで`execArgv`をカラにすることです:

```main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.js', {execArgv: []})
```

こうしておくと、まずはts-node-devでもTypeScriptのワーカーが起動できるようになります。

```
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
tsWorker.ts started
```

しかし、この方法に問題がないわけではありません。ts-node-devの醍醐味としては、TypeScriptのコードを書き直したら、自動的に再コンパイルして、プロセスを起動しなおしてくれることです。しかし、この対処法では、スレッド側でrequireされたファイルをいくら修正しても、自動再コンパイル&再起動はされません。

この課題の解決策については、また時間を見つけて調べてみたいと思います。