Асинхронное программирование в TypeScript: полное руководство на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Асинхронное программирование в 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 позволяет строго типизировать асинхронные коллбэки и использовать асинхронные итераторы для поточной обработки данных.

4.1. Типизация асинхронных функций

Блоги

Рекомендаций по книгам