Asynchronous Programming in TypeScript: A Complete Beginner's Guide
\n\nAsynchronous programming is one of the key concepts in modern web development. If you are just starting to learn TypeScript, understanding how to work with asynchronous code will open up opportunities for creating fast and responsive applications. In this guide, we will cover the basics of asynchronous programming in TypeScript: from simple callbacks to modern async/await, with an emphasis on the strict typing that TypeScript provides.
What is asynchronous programming and why is it needed?
\n\nIn synchronous programming, code executes sequentially: each subsequent operation waits for the previous one to complete. This can lead to the interface "freezing" if, for example, data needs to be loaded from a server. Asynchronous programming allows you to run long-running operations (API requests, file reading, timers) in the background without blocking the main execution thread. TypeScript, being a superset of JavaScript, fully supports asynchronous patterns and adds a powerful type system to them.
\n\nPromise: The Foundation of Asynchrony in TypeScript
\n\nPromise is an object representing the result of an asynchronous operation, which can be in one of three states: pending, fulfilled, or rejected. In TypeScript, we can specify the type of data the promise will return, making the code more predictable.
Example of creating a Promise with typing:
\n\n// Function returning a Promise with type string\nfunction fetchUserData(userId: number): Promise<string> {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n if (userId > 0) {\n resolve(`User data for ID ${userId}`);\n } else {\n reject(new Error("Invalid user ID"));\n }\n }, 1000);\n });\n}\n\n// Using the Promise\nfetchUserData(123)\n .then((data: string) => {\n console.log(data); // "User data for ID 123"\n })\n .catch((error: Error) => {\n console.error(error.message);\n });\n\nNote: we explicitly specified that the promise returns string, and in .then() the data parameter also has the type string. This helps avoid errors at compile time.
Promise Chaining and Error Handling
\n\nPromises can be chained, passing the result of one asynchronous operation to another. TypeScript will automatically infer types at each step.
\n\nExample of a Promise chain:
\n\ninterface User {\n id: number;\n name: string;\n}\n\nfunction getUser(id: number): Promise<User> {\n return new Promise((resolve) => {\n setTimeout(() => resolve({ id, name: "Anna" }), 500);\n });\n}\n\nfunction getPosts(userId: number): Promise<string[]> {\n return new Promise((resolve) => {\n setTimeout(() => resolve(["Post 1", "Post 2"]), 500);\n });\n}\n\n// Call chain\ngetUser(1)\n .then((user: User) => {\n console.log(`User: ${user.name}`);\n return getPosts(user.id); // Return a new Promise\n })\n .then((posts: string[]) => {\n console.log(`Posts: ${posts.join(", ")}`);\n })\n .catch((error: Error) => {\n console.error("Error:", error.message);\n });\n\nIf an error occurs at any stage, execution will jump to the nearest .catch(). This centralized error handling is one of the main advantages of promises over callbacks.
Async/await: Elegant Asynchronous Syntax
\n\nThe async/await syntax makes asynchronous code look like synchronous code, which simplifies reading it