Avatar ·

Как правильно использовать async/await внутри Array.map в TypeScript?

Привет! Я новичок в TypeScript и пытаюсь разобраться с асинхронными операциями внутри метода Array.map. У меня есть массив ID пользователей, и я хочу для каждого ID получить данные пользователя через API (функция fetchUserData возвращает Promise). Я написал такой код: ```typescript const userIds: number[] = [1, 2, 3, 4, 5]; async function fetchUserData(id: number): Promise<{ name: string; age: number }> { const response = await fetch(`https://api.example.com/users/${id}`); return response.json(); } // Моя попытка const users = userIds.map(async (id) => { const userData = await fetchUserData(id); return userData; }); console.log(users); // Выводит массив Promise, а не данных! ``` Проблема в том, что `users` — это массив объектов `Promise`, а не самих данных. Я ожидал получить массив пользователей. Как правильно дождаться выполнения всех асинхронных операций внутри `map` и получить итоговый массив с данными? Я слышал про `Promise.all`, но не понимаю, как его применить в связке с `map`. Буду благодарен за подробное объяснение с примерами на TypeScript! <<>>

Проблема: map возвращает массив Promise, а не данных

Вы столкнулись с классической ловушкой при использовании async/await внутри Array.map. Дело в том, что map — это синхронный метод. Когда вы передаёте ему асинхронную функцию (с пометкой async), она немедленно возвращает Promise, а не дожидается его разрешения. Поэтому map собирает массив из этих Promise, а не из реальных значений.

Решение: комбинируем map с Promise.all

Правильный подход — использовать Promise.all, который принимает массив Promise и возвращает единый Promise, который разрешается, когда все переданные Promise завершены. Вот как это выглядит:

const userIds: number[] = [1, 2, 3, 4, 5];

async function fetchUserData(id: number): Promise<{ name: string; age: number }> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

// Шаг 1: создаём массив Promise через map
const userPromises = userIds.map((id) => fetchUserData(id));

// Шаг 2: дожидаемся всех Promise с помощью Promise.all
const users = await Promise.all(userPromises);

console.log(users); // Теперь это массив объектов с данными пользователей

Полный пример с обработкой ошибок

В реальном проекте важно обрабатывать ошибки. Используйте try/catch:

async function getAllUsers(userIds: number[]): Promise<{ name: string; age: number }[]> {
  try {
    const userPromises = userIds.map((id) => fetchUserData(id));
    const users = await Promise.all(userPromises);
    return users;
  } catch (error) {
    console.error('Ошибка при загрузке пользователей:', error);
    throw error; // пробрасываем ошибку дальше
  }
}

// Использование
(async () => {
  const userIds = [1, 2, 3, 4, 5];
  const users = await getAllUsers(userIds);
  console.log(users);
})();

Важные нюансы

  • Promise.all — fail-fast: Если хотя бы один Promise в массиве завершится с ошибкой, весь Promise.all сразу же упадёт с этой ошибкой. Если вам нужно обработать каждый запрос независимо, используйте Promise.allSettled (ES2020).
  • Типизация: TypeScript автоматически выведет тип результата Promise.all как Promise<{ name: string; age: number }[]>, так что дополнительно указывать типы не нужно.
  • Производительность: Запросы выполняются параллельно, что быстрее последовательного выполнения. Если вам нужно ограничить количество одновременных запросов (например, чтобы не перегрузить API), используйте библиотеки вроде p-limit.
  • Async в map — это нормально: Сам по себе async внутри map не является ошибкой. Ошибка — это не дожидаться результата. Комбинация map + Promise.all — стандартный паттерн.

Альтернатива: for...of с await (последовательное выполнение)

Если вам нужно выполнять запросы строго по очереди (например, для сохранения порядка или из-за ограничений API), используйте цикл for...of:

async function getUsersSequentially(userIds: number[]) {
  const users: { name: string; age: number }[] = [];
  for (const id of userIds) {
    const user = await fetchUserData(id);
    users.push(user);
  }
  return users;
}

Этот подход медленнее, но даёт полный контроль над последовательностью и позволяет обрабатывать ошибки индивидуально.

Вывод

Запомните правило: если вы используете async/await внутри map, результат всегда будет массивом Promise. Чтобы получить массив данных, оберните вызов map в Promise.all и дождитесь его с помощью await.

<<>>

Дополнительный совет: используйте Promise.allSettled для устойчивости к ошибкам

Если вам нужно, чтобы один неудачный запрос не "убивал" весь массив результатов, используйте Promise.allSettled вместо Promise.all. Этот метод возвращает массив объектов с информацией о статусе каждого Promise: fulfilled (успех) или rejected (ошибка).

async function getUsersSafe(userIds: number[]) {
  const results = await Promise.allSettled(
    userIds.map((id) => fetchUserData(id))
  );

  // Фильтруем только успешные результаты
  const users = results
    .filter((result): result is PromiseFulfilledResult<{ name: string; age: number }> => 
      result.status === 'fulfilled'
    )
    .map((result) => result.value);

  // Логируем ошибки
  results
    .filter((result) => result.status === 'rejected')
    .forEach((result) => console.error('Ошибка загрузки:', result.reason));

  return users;
}

Этот подход особенно полезен, когда вы загружаете данные из ненадёжных источников или когда частичный результат лучше, чем полное отсутствие данных. TypeScript поддерживает Promise.allSettled начиная с версии ES2020 — убедитесь, что ваш tsconfig.json настроен соответствующим образом ("target": "ES2020" или выше).