Concurrency and Multithreading in TypeScript: From A to Z
In the modern world of web development, application performance comes to the forefront. TypeScript, being a strictly typed superset of JavaScript, provides developers with powerful tools for working with asynchrony and multithreading. In this article, we will break down the fundamental concepts of concurrency, learn how the Event Loop works, and learn to use Worker Threads for true multithreading in Node.js.
1. Asynchrony and the Event Loop: The Foundation of Concurrency in TypeScript
Before talking about multithreading, it is important to understand how TypeScript (and JavaScript) handles concurrent tasks. At its core is the Event Loop — a mechanism that allows non-blocking I/O operations to be performed without creating new threads.
Let's look at a simple example of asynchronous code:
async function fetchData(url: string): Promise<string> { const response = await fetch(url); return response.text();}
async function main(): Promise<void> { console.log('Start loading'); const data = await fetchData('https://api.example.com/data'); console.log('Data received:', data);}
main();console.log('Code continues executing');In this example, await does not block the main thread. While waiting for a response from the server, the Event Loop continues to process other tasks, such as executing console.log. This is concurrency — the ability to handle multiple tasks within a single time period by switching between them.
2. Promises and async/await: Managing Asynchronous Operations
TypeScript provides convenient syntactic constructs for working with asynchrony. Promises are objects representing the future result of an asynchronous operation. The combination of async/await makes the code linear and readable.
Let's consider a more complex scenario — parallel execution of multiple requests:
async function loadMultipleUsers(userIds: number[]): Promise<User[]> { const promises = userIds.map(id => fetch(`/users/${id}`).then(res => res.json())); return Promise.all(promises);}
// Usageconst users = await loadMultipleUsers([1, 2, 3, 4, 5]);console.log(users); // All users loaded in parallelThe Promise.all method starts all promises simultaneously, which significantly speeds up execution compared to sequential loading. This is an ideal example of concurrency: tasks are executed "simultaneously" within a single thread.
3. True Multithreading: Worker Threads in Node.js
Although asynchrony handles I/O operations well, it does not help with CPU-intensive tasks (image processing, complex calculations, encryption). For such cases, Node.js has Worker Threads — real operating system threads.
Let's look at an example of calculating a factorial in a separate thread:
// worker.tsimport { parentPort } from 'worker_threads';
function factorial(n: number): number { if (n === 0) return 1; return n * factorial(n - 1);}
parentPort?.on('message', (n: number) => { const result = factorial(n); parentPort?.postMessage(result);});
// main.tsimport { Worker } from 'worker_threads';
function runWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker('./worker.ts'); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); });}
async function main() { const result = await runWorker(20); console.log('Factorial of 20:', result);}
main();Each Worker runs in its own thread, has its own Event Loop, and is