← Курс/Awaited и типизация промисов#179 из 257+25 XP

Awaited и типизация промисов

Проблема: вложенные Promise

До TypeScript 4.5 тип Promise<Promise<string>> не разворачивался автоматически. Awaited<T> решает эту проблему:

type A = Awaited<Promise<string>>           // string
type B = Awaited<Promise<Promise<number>>>  // number — рекурсивно!
type C = Awaited<string>                    // string — не Promise, возвращает как есть
type D = Awaited<Promise<string | number>>  // string | number

Реализация Awaited

// Упрощённая реализация из TypeScript source:
type Awaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
    ? F extends (value: infer V, ...args: infer _) => any
      ? Awaited<V>   // рекурсивно разворачиваем
      : never
    : T

Async функции и возвращаемый тип

Async функция всегда возвращает Promise. TypeScript автоматически оборачивает:

// TypeScript знает что async функция возвращает Promise<T>
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// Тип результата:
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>
// = User (не Promise<User>!)

Promise.all и типизация

// TypeScript правильно типизирует Promise.all через кортежи:
const [user, posts, comments] = await Promise.all([
  fetchUser(1),    // Promise<User>
  fetchPosts(1),   // Promise<Post[]>
  fetchComments(1) // Promise<Comment[]>
])
// user: User, posts: Post[], comments: Comment[]

// Promise.allSettled — оборачивает в PromiseSettledResult
const results = await Promise.allSettled([fetchUser(1), fetchPosts(1)])
// PromiseSettledResult<User>[] | PromiseSettledResult<Post[]>[]

Утилиты для работы с промисами

// Тип, который может быть значением или промисом значения
type MaybePromise<T> = T | Promise<T>

async function normalize<T>(value: MaybePromise<T>): Promise<T> {
  return value  // TypeScript понимает что await T | Promise<T> = T
}

// Извлечение типа из MaybePromise:
type Resolved<T> = T extends Promise<infer R> ? R : T

type A = Resolved<Promise<string>>  // string
type B = Resolved<number>           // number

// Async версия функции:
type AsyncVersion<F extends (...args: any[]) => any> =
  (...args: Parameters<F>) => Promise<ReturnType<F>>

Обработка ошибок в промисах

// Паттерн Result для async операций
type AsyncResult<T> = Promise<{ data: T; error: null } | { data: null; error: Error }>

async function safeAsync<T>(promise: Promise<T>): AsyncResult<T> {
  try {
    const data = await promise
    return { data, error: null }
  } catch (err) {
    return { data: null, error: err instanceof Error ? err : new Error(String(err)) }
  }
}

const { data, error } = await safeAsync(fetchUser(1))
if (error) {
  console.error(error.message)
} else {
  console.log(data.name)  // TypeScript знает: data — User
}

Примеры

Утилиты для типобезопасной работы с промисами: safeAsync, retry, timeout, промис-пул

// Показываем паттерны типизированной работы с промисами в JS

// --- safeAsync: Result-паттерн для промисов ---
async function safeAsync(promise) {
  try {
    const data = await promise
    return { data, error: null }
  } catch (err) {
    return { data: null, error: err instanceof Error ? err : new Error(String(err)) }
  }
}

// --- retry: повторяет промис N раз ---
async function retry(fn, attempts = 3, delayMs = 100) {
  let lastError
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (err) {
      lastError = err
      if (i < attempts - 1) {
        await new Promise(r => setTimeout(r, delayMs * (i + 1)))
        console.log(`[retry] попытка ${i + 2} из ${attempts}`)
      }
    }
  }
  throw lastError
}

// --- timeout: отменяет промис через N мс ---
async function withTimeout(promise, ms) {
  let timeoutId
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error(`Превышено время ожидания (${ms}мс)`))
    }, ms)
  })

  try {
    return await Promise.race([promise, timeoutPromise])
  } finally {
    clearTimeout(timeoutId)
  }
}

// --- allSettledTyped: обрабатываем результаты allSettled ---
async function allSettled(promises) {
  const results = await Promise.allSettled(promises)
  return results.reduce((acc, result, i) => {
    if (result.status === 'fulfilled') {
      acc.fulfilled.push({ index: i, value: result.value })
    } else {
      acc.rejected.push({ index: i, reason: result.reason })
    }
    return acc
  }, { fulfilled: [], rejected: [] })
}

// --- Имитация API ---
let fetchAttempt = 0
function mockFetch(url, failTimes = 0) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      fetchAttempt++
      if (fetchAttempt <= failTimes) {
        reject(new Error(`Сеть недоступна (попытка ${fetchAttempt})`))
      } else {
        resolve({ id: 1, name: 'Алексей', email: 'alex@mail.ru', url })
      }
    }, 50)
  })
}

// --- Демонстрация ---
async function main() {
  console.log('=== safeAsync: безопасная обёртка ===')
  const { data, error } = await safeAsync(mockFetch('/api/user'))
  if (error) {
    console.log('Ошибка:', error.message)
  } else {
    console.log('Пользователь:', data.name)
  }

  const { data: d2, error: e2 } = await safeAsync(Promise.reject(new Error('404')))
  console.log('Ошибка (ожидаемая):', e2.message)
  console.log('data при ошибке:', d2)

  console.log('\n=== retry: повтор при ошибке ===')
  fetchAttempt = 0  // сброс счётчика
  try {
    const user = await retry(() => mockFetch('/api/users', 2), 3, 50)
    console.log('Получен после retry:', user.name)
  } catch (e) {
    console.log('Все попытки исчерпаны:', e.message)
  }

  console.log('\n=== timeout: ограничение времени ===')
  try {
    const slow = new Promise(r => setTimeout(() => r('медленный результат'), 300))
    await withTimeout(slow, 100)  // таймаут 100мс < 300мс
  } catch (e) {
    console.log('Таймаут:', e.message)
  }

  const fast = new Promise(r => setTimeout(() => r('быстрый результат'), 10))
  const result = await withTimeout(fast, 200)  // таймаут 200мс > 10мс
  console.log('До таймаута:', result)

  console.log('\n=== Promise.allSettled — группируем результаты ===')
  const responses = await allSettled([
    Promise.resolve({ id: 1, name: 'Alice' }),
    Promise.reject(new Error('Пользователь не найден')),
    Promise.resolve({ id: 3, name: 'Charlie' }),
    Promise.reject(new Error('Сеть недоступна')),
  ])

  console.log('Успешно:', responses.fulfilled.map(r => r.value.name))
  console.log('С ошибками:', responses.rejected.map(r => r.reason.message))
}

main()