Асинхронное программирование в TypeScript: полное руководство для начинающих
Современная веб-разработка невозможна без понимания асинхронного программирования. TypeScript, как надстройка над JavaScript, предоставляет мощные инструменты для работы с асинхронным кодом, делая его более безопасным и предсказуемым. В этом руководстве мы разберем ключевые концепции асинхронного программирования в TypeScript, начиная с основ и заканчивая практическими примерами.
Что такое асинхронное программирование и зачем оно нужно?
В синхронном программировании код выполняется строго последовательно: каждая следующая операция ждет завершения предыдущей. Это создает проблемы при работе с длительными задачами, такими как запросы к серверу, чтение файлов или таймеры. Асинхронное программирование позволяет выполнять такие задачи в фоновом режиме, не блокируя основной поток выполнения.
TypeScript добавляет к асинхронному JavaScript строгую типизацию, что помогает отлавливать ошибки на этапе компиляции. Например, типизированные Promise позволяют точно знать, какой тип данных вернет асинхронная функция.
// Синхронный код - блокирует выполнение
function fetchData(): string {
// Долгая операция
return "данные";
}
// Асинхронный код - не блокирует
async function fetchDataAsync(): Promise<string> {
const response = await fetch('/api/data');
return response.json();
}
Основы: Callback, Promise и Async/Await
Исторически асинхронность в JavaScript начиналась с callback-функций, которые передавались в качестве аргументов. Однако такой подход приводил к "аду колбэков" — глубоко вложенным структурам, сложным для понимания и отладки.
Promises (промисы) решили эту проблему, предоставив цепочечный синтаксис. Promise — это объект, представляющий результат асинхронной операции, который может быть в одном из трех состояний: pending (ожидание), fulfilled (выполнено) или rejected (отклонено).
// Пример Promise в TypeScript
function loadUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
// Использование
loadUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));
Async/Await — это синтаксический сахар над Promises, который делает асинхронный код похожим на синхронный. Ключевое слово async перед функцией указывает, что она возвращает Promise, а await приостанавливает выполнение до получения результата.
// Async/Await в TypeScript
async function getUserData(): Promise<void> {
try {
const user = await loadUser(1);
console.log(user.name); // "Alice"
} catch (error) {
console.error("Ошибка загрузки:", error);
}
}
getUserData();
Типизация асинхронных функций в TypeScript
Одно из главных преимуществ TypeScript — строгая типизация. Для асинхронных функций важно правильно указывать тип возвращаемого Promise. Если функция возвращает строку, тип будет Promise<string>. Компилятор TypeScript проверит, что вы не пытаетесь использовать результат до его получения.
Рассмотрим пример с типизированным API-запросом:
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
async function fetchTodo(id: number): Promise<Todo> {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json() as Todo;
}
// Использование с типизацией
async function displayTodo(): Promise<void> {
const todo = await fetchTodo(1);
console.log(todo.title); // TypeScript знает, что это string
}
Обратите внимание на приведение типа через as Todo. Это гарантирует, что объект будет соответствовать интерфейсу Todo. Однако будьте осторожны: если сервер вернет данные другого формата, TypeScript не выдаст ошибку во время выполнения — только во время компиляции.
Параллельное выполнение и обработка ошибок
Иногда необходимо выполнить несколько асинхронных операций одновременно. Для этого используются Promise.all(), Promise.race() и Promise.allSettled().
- Promise.all() — ждет завершения всех промисов. Если один отклоняется, весь промис отклоняется.
- Promise.race() — возвращает результат первого завершенного промиса (успешного или отклоненного).
- Promise.allSettled() — ждет завершения всех промисов и возвращает массив с результатами каждого.
// Параллельное выполнение
async function loadMultipleUsers(): Promise<void> {
try {
const [user1, user2, user3] = await Promise.all([
loadUser(1),
loadUser(2),
loadUser(3)
]);
console.log("Все пользователи загружены:", user1, user2, user3);
} catch (error) {
console.error("Один из запросов упал:", error);
}
}
// Обработка ошибок с allSettled
async function loadWithPartialFailure(): Promise<void> {
const results = await Promise.allSettled([
loadUser(1),
loadUser(-1), // Этот вызов упадет
loadUser(3)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Пользователь ${index + 1}:`, result.value);
} else {
console.error(`Пользователь ${index + 1} не загружен:`, result.reason);
}
});
}
Для обработки ошибок в асинхронном коде используйте конструкцию try...catch. В TypeScript вы можете создавать собственные классы ошибок для более точной диагностики:
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
async function fetchWithErrorHandling(url: string): Promise<any> {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status, `HTTP ошибка: ${response.statusText}`);
}
return response.json();
}
async function safeFetch(): Promise<void> {
try {
const data = await fetchWithErrorHandling('/api/data');
console.log(data);
} catch (error) {
if (