Перейти к содержимому
Теория · Асинхронный код и данные извне · урок 1 из 10

Promise<T> и async

Чему вы научитесь

Чему вы научитесь:

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

В кафе вы делаете заказ и получаете взамен чек с номером. Блюда ещё нет: его готовят на кухне. Чек — это обещание: когда блюдо будет готово, его выдадут по этому чеку. Съесть чек нельзя, его можно только обменять на блюдо у окна выдачи.

В JavaScript такой чек — промис. Функция, которая долго получает данные, например с сервера, сразу возвращает промис, а само значение приходит позже. TypeScript пишет на чеке, какое блюдо по нему выдадут: Promise<User> — это чек, по которому выдадут пользователя.

Что будет, если перепутать чек с блюдом? Функция loadUser возвращает промис, а код сразу читает у результата имя:

TypeScript
type User = { name: string };

async function loadUser(): Promise<User> {
    return { name: "Anna" };
}

async function main() {
    const user = loadUser();
    console.log(user.name); // ✗
}

main();
TEXT
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 остановит:

TypeScript
async function loadCount(): number { // ✗
    return 41;
}
TEXT
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:

TypeScript
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();

Давайте разберём пример по строкам:

  1. loadUser объявлена с типом результата Promise<User>.
  2. await loadUser() ждёт промис, поэтому у user тип User, а не Promise<User>.
  3. user.name проходит проверку, и программа печатает Anna.
  4. await стоит внутри async-функции main, а main() вызывается в конце файла: так пример работает в любом файле.

В виджете одно и то же значение показано с await и без него. Следите за подсказкой: без await у значения тип промиса, с await — number, а забытый await в арифметике tsc не пропустит.

ВживуюПромис и awaitшаг 1 из 6

Тело возвращает 41, но async заворачивает результат в Promise<number>.

Нажимайте «Следующий шаг»: подсказка покажет тип или ошибку на выделенной строке

Сообщение TS2365 из пятого шага стоит запомнить: оператор + нельзя применить к Promise<number> и 1. В нём стоит '1', а не 'number': TypeScript называет литерал его точным типом. Если в сообщении об ошибке встречается Promise<…> там, где вы ждали значение, почти всегда забыли await.

Здесь аналогия с чеком перестаёт работать. Настоящий чек у окна выдачи забирают, а промис остаётся у вас. Если написать await для того же промиса второй раз, он сразу выдаст то же самое значение.

На собеседовании часто спрашивают, какой тип возвращает async-функция. Ответ: всегда Promise<T>, где T — тип того, что стоит в return, а await даёт значение типа T.

Частая ошибка

Забыли await в условии:

TypeScript
async function isAdmin(): Promise<boolean> {
    return false;
}

async function main() {
    if (isAdmin()) { // ✗
        console.log("admin panel");
    }
}

main();
TEXT
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.