Асинхронное программирование в TypeScript: полное руководство на практике
Современная веб-разработка немыслима без асинхронных операций: запросы к API, работа с базами данных, чтение файлов и таймеры. TypeScript, как надстройка над JavaScript, не только сохраняет все возможности асинхронного программирования, но и добавляет строгую типизацию, делая код более предсказуемым и безопасным. В этом руководстве мы разберём ключевые концепции асинхронности в TypeScript — от базовых Promise до продвинутых паттернов — с реальными примерами кода.
1. Основы: Promise и типизация
Promise — это объект, представляющий результат асинхронной операции, который может быть успешно завершён (fulfilled) или отклонён (rejected). TypeScript позволяет строго типизировать, какое значение вернёт Promise.
// Пример типизированного Promise
function fetchUserData(userId: number): Promise<{ name: string; email: string }> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ name: 'Иван Иванов', email: 'ivan@example.com' });
} else {
reject(new Error('Неверный ID пользователя'));
}
}, 1000);
});
}
// Использование
fetchUserData(1)
.then(user => console.log(user.name)) // Иван Иванов
.catch(error => console.error(error.message));
Обратите внимание: Promise<{ name: string; email: string }> гарантирует, что в then мы получим объект именно с этими полями. Это избавляет от ошибок, связанных с неопределёнными свойствами.
2. Async/Await: синтаксический сахар с TypeScript
Ключевые слова async и await делают асинхронный код похожим на синхронный, не теряя производительности. TypeScript автоматически определяет возвращаемый тип async-функции как Promise<T>.
// Функция возвращает Promise<number>
async function getDiscountPrice(price: number, discountPercent: number): Promise<number> {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Некорректный процент скидки');
}
// Имитация асинхронного расчёта
const discount = await calculateDiscountAsync(price, discountPercent);
return price - discount;
}
// Вспомогательная функция
function calculateDiscountAsync(price: number, percent: number): Promise<number> {
return Promise.resolve(price * percent / 100);
}
// Использование
async function main() {
try {
const finalPrice = await getDiscountPrice(1000, 20);
console.log(`Итоговая цена: ${finalPrice}`); // 800
} catch (error) {
if (error instanceof Error) {
console.error('Ошибка:', error.message);
}
}
}
main();
Важно: TypeScript не позволяет использовать await вне async-функции (кроме верхнего уровня в модулях с определёнными настройками). Всегда оборачивайте вызовы в async функцию или используйте .then().
3. Обработка ошибок и продвинутые паттерны
Грамотная обработка ошибок — основа надёжного асинхронного кода. TypeScript помогает отлавливать ошибки на этапе компиляции, но логику обработки нужно писать вручную.
3.1. Обработка ошибок в async/await
async function safeFetchData(url: string): Promise<unknown> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ошибка! Статус: ${response.status}`);
}
return await response.json();
} catch (error) {
// TypeScript сужает тип до Error
if (error instanceof Error) {
console.error('Ошибка запроса:', error.message);
// Можно вернуть значение по умолчанию или пробросить ошибку дальше
return null;
}
throw error; // Неизвестная ошибка
}
}
3.2. Параллельное выполнение: Promise.all и Promise.allSettled
Когда нужно выполнить несколько независимых асинхронных операций, используйте Promise.all (останавливается при первой ошибке) или Promise.allSettled (ждёт завершения всех).
interface Product {
id: number;
name: string;
price: number;
}
async function loadProducts(): Promise<Product[]> {
// Имитация загрузки с разных источников
const electronics: Promise<Product[]> = Promise.resolve([
{ id: 1, name: 'Ноутбук', price: 50000 }
]);
const clothes: Promise<Product[]> = Promise.resolve([
{ id: 2, name: 'Футболка', price: 1500 }
]);
// Promise.all — быстрее, но упадёт при любой ошибке
const [elecResult, clothResult] = await Promise.all([electronics, clothes]);
return [...elecResult, ...clothResult];
}
// Альтернатива: Promise.allSettled — не останавливается при ошибках
async function loadWithPartialFail(): Promise<void> {
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/invalid-url')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Запрос ${index + 1} успешен:`, result.value);
} else {
console.error(`Запрос ${index + 1} отклонён:`, result.reason);
}
});
}
3.3. Promise.race и таймауты
Promise.race возвращает результат первого завершённого Promise. Это удобно для установки таймаута.
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Таймаут операции')), ms)
);
return Promise.race([promise, timeout]);
}
// Использование
async function fetchWithTimeout(url: string): Promise<Response> {
return withTimeout(fetch(url), 3000);
}
4. Продвинутые практики: типизация коллбэков и асинхронные итераторы
TypeScript позволяет строго типизировать асинхронные коллбэки и использовать асинхронные итераторы для поточной обработки данных.