TypeScriptMiddleTechnical

Что такое утилитный тип Awaited<T>?

Awaited<T> рекурсивно разворачивает Promise: Awaited<Promise<Promise<string>>>string. Используется для вывода типа результата async-функций и совместим с union-типами.

Что такое Awaited<T>

Awaited<T> — встроенный утилитный тип TypeScript (добавлен в 4.5), который рекурсивно разворачивает вложенные Promise и объекты с методом then, имитируя то, что происходит при await в рантайме. До его появления разработчики писали громоздкие условные типы вручную.

Алгоритм разворачивания:

  1. Если Tnull или undefined, вернуть T как есть.
  2. Если T имеет метод then(onfulfilled) (то есть является thenable), взять тип первого аргумента onfulfilled и применить Awaited рекурсивно.
  3. Иначе вернуть T без изменений.

Базовые примеры

type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<number>>>;  // number
type C = Awaited<boolean>;                    // boolean (не Promise — возвращается как есть)
type D = Awaited<Promise<string> | number>;  // string | number (union раскрывается поэлементно)

Реальный пример: типизация ответа API

// api.ts
export async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<{ id: string; name: string; role: "admin" | "user" }>;
}

// Автоматически выводим тип без дублирования интерфейса
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: string; name: string; role: "admin" | "user" }

// Теперь можно типизировать кэш, хранилище, пропсы компонента
const cache = new Map<string, User>();

async function getUser(id: string): Promise<User> {
  if (cache.has(id)) return cache.get(id)!;
  const user = await fetchUser(id);
  cache.set(id, user);
  return user;
}

Пример с Promise.all и union

async function loadDashboard() {
  return Promise.all([
    fetch("/api/user").then(r => r.json() as Promise<{ name: string }>),
    fetch("/api/stats").then(r => r.json() as Promise<{ count: number }>),
  ]);
}

// Тип результата Promise.all — кортеж, Awaited разворачивает каждый элемент
type DashboardData = Awaited<ReturnType<typeof loadDashboard>>;
// [{ name: string }, { count: number }]

// Используем в компоненте
function Dashboard({ data }: { data: DashboardData }) {
  const [user, stats] = data;
  return <div>{user.name}: {stats.count} jobs</div>;
}

Пример с обобщённым хелпером

// Универсальный retry-хелпер сохраняет тип через Awaited
async function withRetry<T>(
  fn: () => Promise<T>,
  attempts = 3
): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
    }
  }
  throw new Error("unreachable");
}

// TypeScript правильно выводит тип без явного указания
const user = await withRetry(() => fetchUser("123"));
// user: { id: string; name: string; role: "admin" | "user" }

Подводные камни

  • Thenable ≠ Promise. Awaited раскрывает любой объект с методом then, не только нативный Promise. Если вы передаёте библиотечный объект-thenable (например, jQuery Deferred), результирующий тип может удивить.
  • Глубокая вложенность скрывает намерение. Awaited<Promise<Promise<T>>> === T, но такой тип — признак архитектурной проблемы (функция возвращает Promise от Promise). Компилятор не выдаёт предупреждений.
  • Не работает как замена infer внутри условных типов. Если вы пишете свой условный тип вроде type Unpack<T> = T extends Promise<infer U> ? U : T, он не рекурсивен. Используйте Awaited вместо него.
  • Потеря null/undefined из union. Awaited<Promise<string> | undefined>string | undefined. Это корректно, но если ваш код предполагал проверку на undefined до await, тип всё равно позволит её пропустить.
  • Не работает с функциями-генераторами. AsyncGenerator — не thenable, его значения Awaited не раскрывает. Для генераторов используйте AsyncGenerator<T> напрямую.
  • Путаница с ReturnType синхронных функций. Если функция не async, но возвращает Promise явно, Awaited<ReturnType<typeof fn>> всё равно корректно раскроет тип. Проблемы нет, но разработчики иногда ошибочно думают, что Awaited применимо только к async-функциям.
  • Версия TypeScript. Awaited добавлен в 4.5. В монорепо со старыми пакетами, которые задают свои tsconfig с "target": "es5" и TS < 4.5, тип будет недоступен — это ломает сборку без очевидного сообщения об ошибке.

Common mistakes

  • Смешивать «Awaited<T>» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «Awaited<T>» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет моделирование результата await и рекурсивное разворачивание Promise.
  • Показывает на примере, как работает: Awaited корректно обрабатывает вложенные Promise и union, поэтому полезен для типов async-функций и библиотечных helpers.
  • Называет production-нюанс и граничный случай для темы «Awaited<T>».

Sources

Related topics