Asynchronous Programming in TypeScript: A Complete Practical Guide
Modern web development is unthinkable without asynchronous operations: API requests, database operations, file reading, and timers. TypeScript, as a superset of JavaScript, not only preserves all the capabilities of asynchronous programming but also adds strict typing, making code more predictable and safer. In this guide, we will break down the key concepts of asynchrony in TypeScript — from basic Promises to advanced patterns — with real code examples.
1. Basics: Promise and Typing
A Promise is an object representing the result of an asynchronous operation, which can be successfully completed (fulfilled) or rejected (rejected). TypeScript allows you to strictly type what value the Promise will return.
// Example of a typed Promisefunction fetchUserData(userId: number): Promise<{ name: string; email: string }> { return new Promise((resolve, reject) => { setTimeout(() => { if (userId > 0) { resolve({ name: 'Ivan Ivanov', email: 'ivan@example.com' }); } else { reject(new Error('Invalid user ID')); } }, 1000); });}
// UsagefetchUserData(1) .then(user => console.log(user.name)) // Ivan Ivanov .catch(error => console.error(error.message));Note: Promise<{ name: string; email: string }> guarantees that in the then callback we will receive an object with exactly these fields. This eliminates errors related to undefined properties.
2. Async/Await: Syntactic Sugar with TypeScript
The async and await keywords make asynchronous code look like synchronous code without losing performance. TypeScript automatically infers the return type of an async function as Promise<T>.
// Function returns Promise<number>async function getDiscountPrice(price: number, discountPercent: number): Promise<number> { if (discountPercent < 0 || discountPercent > 100) { throw new Error('Invalid discount percentage'); } // Simulating an asynchronous calculation const discount = await calculateDiscountAsync(price, discountPercent); return price - discount;}
// Helper functionfunction calculateDiscountAsync(price: number, percent: number): Promise<number> { return Promise.resolve(price * percent / 100);}
// Usageasync function main() { try { const finalPrice = await getDiscountPrice(1000, 20); console.log(`Final price: ${finalPrice}`); // 800 } catch (error) { if (error instanceof Error) { console.error('Error:', error.message); } }}main();Important: TypeScript does not allow using await outside an async function (except at the top level in modules with specific settings). Always wrap calls in an async function or use .then().
3. Error Handling and Advanced Patterns
Proper error handling is the foundation of reliable asynchronous code. TypeScript helps catch errors at compile time, but the handling logic needs to be written manually.
3.1. Error Handling in async/await
async function safeFetchData(url: string): Promise<unknown> { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return await response.json(); } catch (error) { // TypeScript narrows the type to Error if (error instanceof Error) { console.error('Request error:', error.message); // Can return a default value or rethrow the error return null; } throw error; // Unknown error