Promise<T> и async
Чему вы научитесь:
- читать тип
Promise<T>и объяснять, почемуasync-функция всегда возвращает промис; - получать значение типа
Tчерезawaitи узнавать забытыйawaitпо сообщениюtsc; - типизировать
Promise.allи ошибку вcatch, у которой типunknown; - проверять данные извне предикатом типа и отличать такую проверку от
as.

В кафе вы делаете заказ и получаете взамен чек с номером. Блюда ещё нет: его готовят на кухне. Чек — это обещание: когда блюдо будет готово, его выдадут по этому чеку. Съесть чек нельзя, его можно только обменять на блюдо у окна выдачи.
В JavaScript такой чек — промис. Функция, которая долго получает данные, например с сервера, сразу возвращает промис, а само значение приходит позже. TypeScript пишет на чеке, какое блюдо по нему выдадут: Promise<User> — это чек, по которому выдадут пользователя.
Что будет, если перепутать чек с блюдом? Функция loadUser возвращает промис, а код сразу читает у результата имя:
type User = { name: string };
async function loadUser(): Promise<User> {
return { name: "Anna" };
}
async function main() {
const user = loadUser();
console.log(user.name); // ✗
}
main();error TS2339: Property 'name' does not exist on type 'Promise<User>'.Сообщение говорит: у типа Promise<User> нет свойства name. В user лежит чек, а не пользователь. Без проверки типов программа не упала бы, а молча напечатала бы undefined.
Тип Promise<T>
Как вы уже знаете из темы про дженерики, Promise<T> — обобщённый тип. В угловых скобках стоит тип значения, которое промис выдаст, когда завершится:
Promise<number>выдаст число;Promise<User>выдаст объект типаUser;Promise<void>ничего полезного не выдаст, а только сообщит, что работа закончена.
Сам промис и значение внутри него — разные вещи. У промиса есть методы then и catch, а свойств пользователя у него нет. Поэтому tsc и не дал прочитать user.name.
async-функция возвращает промис
Слово async перед функцией меняет её тип результата. Что бы ни стояло в return, вызывающий код получит промис с этим значением:
- функция с
return 41возвращаетPromise<number>; - функция без
returnвозвращаетPromise<void>.
Так бывает, даже когда значение готово сразу. Кафе всё равно выдаёт чек, хотя блюдо уже стоит на стойке.
Поэтому тип результата async-функции записывают как промис. Если написать тип самого значения, tsc остановит:
async function loadCount(): number { // ✗
return 41;
}error TS1064: The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?В конце сообщения tsc подсказывает правильную запись: Promise<number>. Тип результата можно и не писать: TypeScript сам выведет Promise<number> из return 41.
await разворачивает промис
Чтобы получить блюдо, чек обменивают у окна выдачи. В коде для этого есть await: он ждёт, пока промис завершится, и выдаёт значение. Для TypeScript это значит, что await превращает Promise<T> обратно в T:
type User = { name: string };
async function loadUser(): Promise<User> {
return { name: "Anna" };
}
async function main() {
const user = await loadUser();
console.log(user.name); // Anna
}
main();Давайте разберём пример по строкам:
loadUserобъявлена с типом результатаPromise<User>.await loadUser()ждёт промис, поэтому уuserтипUser, а неPromise<User>.user.nameпроходит проверку, и программа печатаетAnna.awaitстоит внутриasync-функцииmain, аmain()вызывается в конце файла: так пример работает в любом файле.
В виджете одно и то же значение показано с await и без него. Следите за подсказкой: без await у значения тип промиса, с await — number, а забытый await в арифметике tsc не пропустит.
1async function loadCount() {подсказка редактораloadCount: () => Promise<number>2 return 41;3}4 5async function main() {6 const pending = loadCount();7 const count = await pending;8 console.log(count + 1);9 console.log(loadCount() + 1);10}11 12main();
Тело возвращает 41, но async заворачивает результат в Promise<number>.
Сообщение TS2365 из пятого шага стоит запомнить: оператор + нельзя применить к Promise<number> и 1. В нём стоит '1', а не 'number': TypeScript называет литерал его точным типом. Если в сообщении об ошибке встречается Promise<…> там, где вы ждали значение, почти всегда забыли await.
Здесь аналогия с чеком перестаёт работать. Настоящий чек у окна выдачи забирают, а промис остаётся у вас. Если написать await для того же промиса второй раз, он сразу выдаст то же самое значение.
На собеседовании часто спрашивают, какой тип возвращает async-функция. Ответ: всегда Promise<T>, где T — тип того, что стоит в return, а await даёт значение типа T.
Частая ошибка
Забыли await в условии:
async function isAdmin(): Promise<boolean> {
return false;
}
async function main() {
if (isAdmin()) { // ✗
console.log("admin panel");
}
}
main();error TS2801: This condition will always return true since this 'Promise<boolean>' is always defined.Сообщение говорит: условие всегда истинно, потому что промис всегда есть. Чек — это объект, а объект в if всегда считается истинным, что бы по нему потом ни выдали. Без проверки типов программа показала бы панель администратора каждому, хотя isAdmin возвращает false. Нужно дождаться ответа: if (await isAdmin()).
Главное
Promise<T>— промис, который выдаст значение типаT; свойств этого значения у самого промиса нет.async-функция всегда возвращаетPromise<T>, и тип её результата пишут какPromise<number>, а неnumber.awaitразворачиваетPromise<T>обратно вT.Promise<…>в сообщенииTS2339,TS2365илиTS2801там, где ждали значение, почти всегда значит забытыйawait.