Утилітний тип awaited у TypeScript
Awaited
Теорія
TL;DR
- Аналогія: кожен Promise - це обгортка. Awaited знімає всі обгортки, скільки б шарів не було.
- Основна поведінка: розгортає одиночні Promise, вкладені Promise та union-типи. Якщо T не є Promise - повертає T без змін.
- Правило вибору:
Awaited<ReturnType<typeof fn>>- стандартний патерн для типізації async-функцій. - Доступний з TypeScript 4.5.
Швидкий приклад
// Один шар
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 до функції замість її типу повернення:
type Fn = () => Promise<number>;
type Wrong = Awaited<Fn>; // never
type Right = Awaited<ReturnType<Fn>>; // numberТип функції не розширює Promise<infer U>, тому Awaited повертає never. Завжди використовуй ReturnType спочатку.
Самописний однорівневий unwrap, що ламається на вкладених Promise:
// Старий патерн (до 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-функціями:
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 розгортатиме поля об'єкта:
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:
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-функції
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>>; // UserReturnType витягує те, що функція повертає, включно з Promise. Awaited знімає обгортку. Разом це стандартний патерн для типізації async-результатів у продакшен-коді.
Типізація хука React Query
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
type Complex = Promise<Promise<string> | void>;
type Resolved = Awaited<Complex>;
// Крок 1: Awaited<Promise<string> | void>
// Крок 2: Awaited<Promise<string>> | Awaited<void>
// Крок 3: string | voidAwaited<void> залишається void, а не never. Якщо треба прибрати void з union-результату - використовуй Exclude<Resolved, void>, що дасть string. Awaited і Exclude - дві окремі операції.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.