Введение в асинхронное программирование на TypeScript
Асинхронное программирование — это подход к написанию кода, при котором операции, требующие времени (например, запросы к серверу, чтение файлов, таймеры), не блокируют выполнение основного потока программы. В современной веб-разработке асинхронность — это не просто удобство, а необходимость. JavaScript и TypeScript, будучи однопоточными языками, используют цикл событий (Event Loop) для обработки асинхронных операций.
TypeScript добавляет к этой мощной парадигме статическую типизацию, что делает код более предсказуемым и безопасным. В этом руководстве мы разберем все аспекты асинхронного программирования: от классических колбэков до современных async/await с полной типизацией.
Основы асинхронности: от колбэков к промисам
Проблема колбэков и «ад колбэков»
Изначально асинхронность в JavaScript реализовывалась через колбэки (функции обратного вызова). Однако такой подход быстро приводил к нечитаемому коду:
function loadData(callback: (data: string) => void) {
setTimeout(() => {
callback("Данные загружены");
}, 1000);
}
loadData((data) => {
console.log(data);
// Вложенный вызов
loadData((data2) => {
console.log(data2);
});
});
Такой код сложно отлаживать и поддерживать. TypeScript не решает проблему вложенности, но помогает типизировать параметры колбэков.
Промисы (Promise) — спасение от ада
Промис представляет собой объект, который может находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнено успешно) или rejected (отклонено с ошибкой). TypeScript позволяет строго типизировать тип данных, которые вернет промис:
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Иван Петров" });
} else {
reject(new Error("Некорректный ID"));
}
}, 500);
});
}
interface User {
id: number;
name: string;
}
// Использование
fetchUser(1).then(user => {
console.log(user.name); // TypeScript знает, что user — это User
}).catch(error => {
console.error(error.message);
});
Благодаря дженерику Promise<User> компилятор TypeScript гарантирует, что внутри .then() мы получим объект именно этого типа.
Async/Await: элегантная асинхронность
Синтаксис и типизация
Ключевые слова async и await позволяют писать асинхронный код, который выглядит как синхронный. Функция, помеченная async, всегда возвращает промис:
async function getGreeting(name: string): Promise<string> {
return `Привет, ${name}!`;
}
// Тип возвращаемого значения — Promise<string>
const result: Promise<string> = getGreeting("Мир");
Важно помнить: если async функция возвращает не промис, TypeScript автоматически оборачивает его в промис.
Пример с реальной логикой
Рассмотрим типичный сценарий — получение данных с API и их обработка:
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function fetchPosts(userId: number): Promise<Post[]> {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
if (!response.ok) {
throw new Error(`Ошибка HTTP: ${response.status}`);
}
const data: Post[] = await response.json();
return data;
}
async function displayPosts() {
try {
const posts = await fetchPosts(1);
console.log(`Получено ${posts.length} постов:`);
posts.forEach(post => console.log(`- ${post.title}`));
} catch (error) {
if (error instanceof Error) {
console.error("Не удалось загрузить посты:", error.message);
}
}
}
displayPosts();
Обратите внимание на типизацию: response.json() возвращает Promise<any>, но мы явно указываем Post[], чтобы TypeScript проверил корректность дальнейшего использования данных.
Параллельное выполнение и обработка ошибок
Promise.all, Promise.race и Promise.allSettled
Часто требуется выполнить несколько асинхронных операций параллельно. Для этого существуют статические методы класса Promise. TypeScript корректно выводит типы при их использовании:
async function loadAllUsers(): Promise<User[]> {
const ids = [1, 2, 3, 4, 5];
// Promise.all возвращает Promise<[User, User, User, User, User]>
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users; // TypeScript знает, что это User[]
}
// Promise.race — первый завершенный (успех или ошибка)
async function getFastestResponse(): Promise<string> {
const promises = [
fetch("https://api1.example.com").then(r => r.text()),
fetch("https://api2.example.com").then(r => r.text()),
];
return Promise.race(promises);
}
// Promise.allSettled — дожидается всех, даже если некоторые упали
async function checkAllServices(): Promise<void> {
const results = await Promise.allSettled([
fetch("https://service1.com/health"),
fetch("https://service2.com/health"),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Сервис ${index + 1}: OK`);
} else {
console.error(`Сервис ${index + 1}: ${result.reason}`);
}
});
}
Типизированная обработка ошибок
В TypeScript рекомендуется создавать пользовательские классы ошибок для лучшего контроля:
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = "NetworkError";
}
}
async function safeFetch(url: string): Promise<Response> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(
`Ошибка запроса к ${url}`,
response.status
);
}
return response;
} catch (error) {
if (error instanceof NetworkError) {
throw error; // Пробра