Avatar ·

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

Всем привет! Я начинающий TypeScript разработчик, и у меня возникла проблема с использованием async/await вместе с методом `Array.map`. Я пишу функцию, которая должна обработать массив URL-адресов, загрузить данные из каждого и вернуть массив результатов. Вот мой код на TypeScript: ```typescript interface Post { id: number; title: string; } async function fetchPost(id: number): Promise { const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); return response.json(); } async function fetchAllPosts(ids: number[]): Promise { return ids.map(async (id) => { const post = await fetchPost(id); return post; }); } const result = await fetchAllPosts([1, 2, 3]); console.log(result); // Что я получу? ``` Когда я запускаю этот код, я ожидаю получить массив объектов `Post`, но вместо этого получаю массив промисов `Promise[]`. Я понимаю, что `map` не ждет завершения асинхронных операций, но как мне правильно дождаться всех результатов? Я слышал про `Promise.all`, но не уверен, как его применить в этом случае. Также меня интересует, есть ли способ обрабатывать ошибки для каждого запроса отдельно. Подскажите, пожалуйста, как правильно написать `fetchAllPosts`, чтобы она возвращала `Promise` с уже загруженными данными, а не массив промисов? Заранее спасибо! <<>>

Проблема: map не ожидает async функции

Вы столкнулись с классической проблемой: метод Array.map синхронный и не умеет работать с промисами. Когда вы передаете асинхронную функцию (async), она немедленно возвращает промис (Promise), а не сам результат. Поэтому map возвращает массив промисов Promise[], а не Post[].

Решение: комбинация map + Promise.all

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

async function fetchAllPosts(ids: number[]): Promise<Post[]> {
  // 1. Создаем массив промисов с помощью map
  const promises = ids.map(async (id) => {
    const post = await fetchPost(id);
    return post;
  });

  // 2. Дожидаемся всех промисов сразу
  const posts = await Promise.all(promises);
  return posts;
}

// Или более краткий вариант:
async function fetchAllPostsShort(ids: number[]): Promise<Post[]> {
  return await Promise.all(ids.map(id => fetchPost(id)));
}

// Использование:
const result = await fetchAllPosts([1, 2, 3]);
console.log(result); // ✅ [Post, Post, Post]

Как это работает

  • map создает массив промисов (каждый вызов fetchPost возвращает промис)
  • Promise.all принимает этот массив и возвращает новый промис
  • Мы ждем его с помощью await, получая массив результатов
  • Все запросы выполняются параллельно, что быстрее последовательного выполнения

Обработка ошибок

Если один из запросов упадет, Promise.all сразу завершится с ошибкой (режим "fail-fast"). Чтобы обрабатывать ошибки для каждого запроса отдельно, используйте Promise.allSettled (ES2020) или оборачивайте каждый запрос в try/catch:

// Вариант 1: Promise.allSettled (получаем статус каждого запроса)
async function fetchAllPostsWithStatus(ids: number[]): Promise<(Post | null)[]> {
  const results = await Promise.allSettled(
    ids.map(id => fetchPost(id))
  );

  return results.map(result => {
    if (result.status === 'fulfilled') {
      return result.value; // успешный результат
    } else {
      console.error('Ошибка загрузки:', result.reason);
      return null; // или выбрасываем ошибку
    }
  });
}

// Вариант 2: try/catch внутри map
async function fetchAllPostsSafe(ids: number[]): Promise<Post[]> {
  const results = await Promise.all(
    ids.map(async (id) => {
      try {
        return await fetchPost(id);
      } catch (error) {
        console.error(`Ошибка загрузки поста ${id}:`, error);
        // Можно вернуть значение по умолчанию или null
        return { id, title: 'Ошибка загрузки', body: '' } as Post;
      }
    })
  );
  return results;
}

Важные замечания

  • Типизация: TypeScript автоматически выводит тип Promise<Post[]> из Promise.all
  • Производительность: Все запросы выполняются параллельно. Если нужно ограничить количество одновременных запросов, используйте библиотеки вроде p-limit
  • Альтернатива: Для последовательного выполнения (один запрос за другим) используйте цикл for...of с await внутри

Теперь ваша функция будет работать корректно и возвращать массив постов, а не промисов!

<<>>

Дополнительный подход: последовательное выполнение с for...of

Иногда вам нужно выполнять асинхронные операции последовательно, а не параллельно. Например, если вы делаете запросы к API с ограничением по частоте (rate limiting) или каждый следующий запрос зависит от предыдущего. В этом случае Promise.all не подходит, и лучше использовать цикл for...of:

async function fetchAllPostsSequential(ids: number[]): Promise<Post[]> {
  const posts: Post[] = [];

  for (const id of ids) {
    const post = await fetchPost(id);
    posts.push(post);
  }

  return posts;
}

// Использование:
const result = await fetchAllPostsSequential([1, 2, 3]);
// Запросы выполняются по очереди: сначала пост 1, потом 2, потом 3

Когда использовать

  • for...of + await: когда важен порядок выполнения или есть ограничения на количество запросов
  • map + Promise.all: когда запросы независимы и нужно максимальное быстродействие

Также обратите внимание на Array.reduce для последовательной цепочки промисов, но это менее читаемый вариант. Для большинства случаев с независимыми запросами используйте первый ответ с Promise.all — это и быстрее, и проще для понимания.