Skip to main content

Утилітний тип awaited у TypeScript

Awaited - утилітний тип TypeScript, доступний з версії 4.5, який рекурсивно розгортає ланцюги Promise і повертає тип фінального значення.

Теорія

TL;DR

  • Аналогія: кожен Promise - це обгортка. Awaited знімає всі обгортки, скільки б шарів не було.
  • Основна поведінка: розгортає одиночні Promise, вкладені Promise та union-типи. Якщо T не є Promise - повертає T без змін.
  • Правило вибору: Awaited<ReturnType<typeof fn>> - стандартний патерн для типізації async-функцій.
  • Доступний з TypeScript 4.5.

Швидкий приклад

typescript
// Один шар type A = Awaited<Promise<string>>; // string // Вкладені - TypeScript рекурсує автоматично type B = Awaited<Promise<Promise<number>>>; // number // Union - розподіляється по кожному члену type C = Awaited<Promise<string> | number>; // string | number // Практичний приклад з async-функцією async function fetchUser(): Promise<{ id: number }> { return { id: 42 }; } type UserType = Awaited<ReturnType<typeof fetchUser>>; // { id: number }

TypeScript знімає шари Promise один за одним, доки не дійде до звичайного типу. Важливий момент з union: Awaited<Promise<A> | B> дає A | B, а не Promise<A | B>.

Як працює рекурсивне розгортання

TypeScript реалізує Awaited як умовний (conditional) тип: якщо T extends Promise<infer U> - рекурсує на Awaited<U>, інакше повертає T. Все відбувається на рівні компілятора під час перевірки типів. V8 і Node цього не бачать.

Розподіл по union вбудований. Awaited<Promise<A> | Promise<B>> стає A | B, бо TypeScript застосовує умовний тип до кожного члена union окремо. Це корисно в API-клієнтах, де функція може повернути Promise<ResponseA> | Promise<ResponseB> залежно від параметрів.

Особисто бачив, як це ловить розробників, що працюють зі старими SDK-обгортками, які повертають Promise<Promise<T>> через подвійне загортання. Awaited впорається з цим без жодних змін з твого боку.

Коли використовувати

  • Типізація повернення async-функцій: Awaited<ReturnType<typeof fn>> дає чистий тип без ручних тверджень типу.
  • API-ланцюги: коли глибина вкладеності Promise залежить від SDK або версії бібліотеки.
  • Union async-результатів: Awaited<Promise<string> | Promise<number>> згортається до string | number.
  • Типізація Promise.all: у парі з mapped type по кортежу для розгортання кожного елемента.

Не варто використовувати, якщо T вже є звичайним типом. Awaited<string> повертає string - технічно правильно, але користі нуль.

Типові помилки

Застосування Awaited до функції замість її типу повернення:

typescript
type Fn = () => Promise<number>; type Wrong = Awaited<Fn>; // never type Right = Awaited<ReturnType<Fn>>; // number

Тип функції не розширює Promise<infer U>, тому Awaited повертає never. Завжди використовуй ReturnType спочатку.

Самописний однорівневий unwrap, що ламається на вкладених Promise:

typescript
// Старий патерн (до TypeScript 4.5) type OldWay<T> = T extends Promise<infer U> ? U : T; type A = OldWay<Promise<Promise<string>>>; // Promise<string> - ще обгорнуто type B = Awaited<Promise<Promise<string>>>; // string - правильно

Самописна версія не рекурсує. Якщо бачиш такий патерн у легасі-коді - заміни на Awaited.

Забутий ReturnType при роботі з async-функціями:

typescript
async function loadData() { return { id: 1, name: "Alice" }; } type T1 = ReturnType<typeof loadData>; // Promise<{ id: number; name: string }> type T2 = Awaited<ReturnType<typeof loadData>>; // { id: number; name: string }

Очікування, що Awaited розгортатиме поля об'єкта:

typescript
type Obj = { data: Promise<string> }; type T = Awaited<Obj>; // Obj - без змін

Awaited розгортає тільки верхній рівень Promise. Вкладені Promise всередині об'єктів залишаються як є. Для цього потрібен рекурсивний mapped type.

Реальне використання

  • TanStack Query: type Data = Awaited<ReturnType<typeof fetchUser>> для типізації даних хука без ручних тверджень типу.
  • tRPC: inferRouterOutputs<AppRouter> використовує Awaited під капотом для типізації результатів процедур.
  • Express async-хендлери: Awaited<ReturnType<typeof handler>> для виведення типу повернення middleware.
  • Zod з async-трансформами: коли схема містить async-перетворення, Awaited правильно розв'язує тип виходу.

Питання на співбесіді

Q: Що повертає Awaited<Promise<void>>?
A: void. TypeScript не перетворює void на never. Це правильна поведінка для fire-and-forget функцій: awaiting їх дає void, і Awaited це відображає.

Q: Як Awaited розподіляється по union-типах?
A: Awaited<Promise<A> | B> стає Awaited<Promise<A>> | Awaited<B>, тобто A | B. TypeScript застосовує умовний тип до кожного члена union окремо.

Q: Чим Awaited відрізняється від старого ручного патерну розгортання?
A: Ручний патерн T extends Promise<infer U> ? U : T знімає рівно один шар. Awaited рекурсує до не-Promise типу і підтримує union-розподіл.

Q: Реалізуй Awaited вручну без вбудованого типу.
A:

typescript
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

Це покриває рекурсію. Розподіл по union TypeScript виконує автоматично для дистрибутивних умовних типів.

Q: Чому в React Query краще Awaited<ReturnType<typeof fn>>, ніж ручна анотація data?
A: Тому що тип відстежує сигнатуру queryFn автоматично. Зміниш тип повернення функції - тип data оновиться разом із нею. Ручна анотація може застаріти без жодного попередження компілятора.

Приклади

Базовий: отримання типу з async-функції

typescript
interface User { id: number; name: string; } async function fetchUser(id: number): Promise<User> { const res = await fetch(`/api/users/${id}`); return res.json(); } // Тільки ReturnType - Promise залишається type Wrapped = ReturnType<typeof fetchUser>; // Promise<User> // Додаємо Awaited - шар Promise знятий type Resolved = Awaited<ReturnType<typeof fetchUser>>; // User

ReturnType витягує те, що функція повертає, включно з Promise. Awaited знімає обгортку. Разом це стандартний патерн для типізації async-результатів у продакшен-коді.

Типізація хука React Query

typescript
interface Post { id: number; title: string; } async function fetchPost(id: number): Promise<Post> { const res = await fetch(`/api/posts/${id}`); return res.json(); } type PostData = Awaited<ReturnType<typeof fetchPost>>; // Post function usePost(id: number) { return useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id), // data автоматично виводиться як Post | undefined }); }

TanStack Query v5 виводить тип data з queryFn через Awaited під капотом. Ти отримуєш Post | undefined без жодної ручної анотації типу на хуку.

Граничний випадок: union з void

typescript
type Complex = Promise<Promise<string> | void>; type Resolved = Awaited<Complex>; // Крок 1: Awaited<Promise<string> | void> // Крок 2: Awaited<Promise<string>> | Awaited<void> // Крок 3: string | void

Awaited<void> залишається void, а не never. Якщо треба прибрати void з union-результату - використовуй Exclude<Resolved, void>, що дасть string. Awaited і Exclude - дві окремі операції.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?