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:
1import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";2
3// the worker is the current file4const workerFileName = __filename5
6// define a type for the data we want to share7type WorkerData = {8  id: number,9  name: string10}11
12// check if we're on the main or worker thread13if (isMainThread) {14  // launch a worker thread15  const worker = new Promise((resolve, reject) => {16    const worker = new Worker(workerFileName, {17      // pass data to the worker18      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 thread31  console.log("Running in worker", workerData as WorkerData)32  const message = `Hello ${workerData.name} from worker`33
34  // return back to the parent thread35  parentPort?.postMessage({ message })36}In the above file we can see a few things that are important to using workers:
- A check is done using isMainThreadto determine if the file is being executed as a worker. the handling for the worker is based on this
- A reference to the file which should be run in the worker thread, the __filenamevariable 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
- A worker is created using new Worker, since the worker is an event based API in the above example it’s being wrapped in aPromise
- Data can be passed to the worker via the workerDataproperty, this will then be put into theworkerDatavariable fromnode:worker_threadsin the worker thread
- Data is sent back to the worker using parentPort.postMessage. This is the same even that’s being listened for thePromiseto resolve inworker.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
1import { isMainThread, parentPort, workerData } from "node:worker_threads";2
3export const workerFileName = __filename4
5export type WorkerData = {6  id: number,7  name: string8}9
10if (!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
1import { Worker } from "node:worker_threads"2
3// import the worker file name from where we defined the worker4import { workerFileName } from "./worker"5
6const 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
18async function main() {19  const results = await Promise.all(workers)20  console.log(results)21}22
23main()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:
1const worker = new Worker("my-worker.ts", {2    execArgv: ["-r", "ts-node/register", "--no-warnings"],3    workerData: {4      id5    }6  })7
8// do stuff with the worker