Typescript Workers in NodeJS

12 February 2025

Updated: 19 February 2025

Using worker threads with Typescript is pretty straightforward, this probably depends a bit on your setup but the below idea seems to work for me

Single File Worker

In NodeJS we can use a sort of self-referrential worker such that depending on the context in which the file is loaded the behavior will differ, for example:

main.ts
1
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
2
3
// the worker is the current file
4
const workerFileName = __filename
5
6
// define a type for the data we want to share
7
type WorkerData = {
8
id: number,
9
name: string
10
}
11
12
// check if we're on the main or worker thread
13
if (isMainThread) {
14
// launch a worker thread
15
const worker = new Promise((resolve, reject) => {
16
const worker = new Worker(workerFileName, {
17
// pass data to the worker
18
workerData: {
19
id: "my-worker",
20
name: "Bob"
21
}
22
})
23
24
worker.addListener("message", (result) => resolve(result))
25
worker.addListener("error", reject)
26
})
27
28
worker.then(console.log)
29
} else {
30
// do stuff for the worker thread
31
console.log("Running in worker", workerData as WorkerData)
32
const message = `Hello ${workerData.name} from worker`
33
34
// return back to the parent thread
35
parentPort?.postMessage({ message })
36
}

In the above file we can see a few things that are important to using workers:

  1. A check is done using isMainThread to determine if the file is being executed as a worker. the handling for the worker is based on this
  2. A reference to the file which should be run in the worker thread, the __filename variable is defined by NodeJS and holds the name of the curent file - this will be the compiled file name so we don’t need to figure that out on our own
  3. A worker is created using new Worker, since the worker is an event based API in the above example it’s being wrapped in a Promise
  4. Data can be passed to the worker via the workerData property, this will then be put into the workerData variable from node:worker_threads in the worker thread
  5. Data is sent back to the worker using parentPort.postMessage. This is the same even that’s being listened for the Promise to resolve in worker.addListener("message", resolve)

Multi File Worker

Extending the above to work with multiple files is pretty simple, the only additional trick we need is to export the __filename from our worker file to the place where we want to construct the worker

So, the updated code can then be broken into two files as follows:

First, the worker is pretty similar to the one above, except now we export the __filename as workerFileName

worker.ts
1
import { isMainThread, parentPort, workerData } from "node:worker_threads";
2
3
export const workerFileName = __filename
4
5
export type WorkerData = {
6
id: number,
7
name: string
8
}
9
10
if (!isMainThread) {
11
console.log("Running in worker", workerData)
12
const message = `Hello ${workerData.name} from worker`
13
parentPort?.postMessage({ message })
14
} else {
15
console.log("Worker was run in the main thread")
16
}

The main.ts file then makes use of this exported file name to invoke the worker. Note that this is now just a normal script and will be executed in the main thread as normal

main.ts
1
import { Worker } from "node:worker_threads"
2
3
// import the worker file name from where we defined the worker
4
import { workerFileName } from "./worker"
5
6
const workers = [1, 2, 3, 4].map(id => new Promise((resolve, reject) => {
7
const worker = new Worker(workerFileName, {
8
workerData: {
9
id,
10
name: `Bob ${id}`
11
}
12
})
13
14
worker.addListener("message", (result) => resolve({ id, result }))
15
worker.addListener("error", reject)
16
}))
17
18
async function main() {
19
const results = await Promise.all(workers)
20
console.log(results)
21
}
22
23
main()

Uncompiled Worker Using ts-node

When executing workers it’s also possible to execute the Typescript file directly instead of relying on the compilation. This can be useful for cases where the Javascript code is compiled to a single file or in situations where you don’t have control over the execution environment

Executing a Typescript worker can be done by using ts-node when running the worker, an example of this is:

uncompiled-ts-worker.ts
1
const worker = new Worker("my-worker.ts", {
2
execArgv: ["-r", "ts-node/register", "--no-warnings"],
3
workerData: {
4
id
5
}
6
})
7
8
// do stuff with the worker

References