Promise.all и колбэки
В прошлом уроке был один заказ и один чек. Часто заказов несколько: пользователь и число его покупок приходят с сервера по отдельности.
Представьте, что в кафе вы заказали суп у одного окна и чай у другого. Ждать их по очереди незачем: обе кухни работают одновременно. Удобнее сдать оба чека сразу и получить поднос, на котором стоят оба блюда — в том порядке, в каком вы сдали чеки.
Promise.all собирает такой поднос. Он принимает массив промисов и возвращает один промис, который выдаст массив результатов в том же порядке:
type User = { name: string };
async function loadUser(): Promise<User> {
return { name: "Anna" };
}
async function loadCount(): Promise<number> {
return 3;
}
async function main() {
const [user, count] = await Promise.all([loadUser(), loadCount()]);
console.log(user.name, count); // Anna 3
}
main();Давайте разберём, что происходит в main:
loadUser()иloadCount()возвращаютPromise<User>иPromise<number>.Promise.all([...])получает оба промиса: обе функции уже вызваны и работают одновременно.awaitвыдаёт поднос: массив, где первым стоит пользователь, а вторым — число.- Деструктуризация
[user, count]раскладывает его по переменным, и программа печатаетAnna 3.
Поднос — это кортеж
Тип у подноса точный. Если сохранить результат в одну переменную, const pair = await Promise.all(...), и навести курсор на pair, подсказка редактора покажет [User, number]. Это кортеж: у каждой позиции свой тип.
- позиция 0 —
User, потому что первым в массиве стоитloadUser(); - позиция 1 —
number, потому что вторым стоитloadCount().
Поэтому user.name проходит проверку без единой аннотации. Будь тип массивом (number | User)[], как у обычного литерала из двух разных значений, каждый элемент пришлось бы сначала проверять. TypeScript не путает промисы местами: тип каждого результата он берёт из промиса на той же позиции.
API на колбэках
Промисы появились в JavaScript не сразу. Многие старые библиотеки и часть функций Node.js сообщают результат иначе — через колбэк, который вызывают, когда работа закончена:
type User = { name: string };
function loadUser(done: (err: Error | null, user?: User) => void): void {
setTimeout(() => done(null, { name: "Anna" }), 10);
}
loadUser((err, user) => {
if (err === null && user !== undefined) {
console.log(user.name); // Anna
}
});Где здесь тип результата? Не в типе самой функции: loadUser возвращает void. Результат описан в параметрах колбэка done:
err: Error | null— ошибка илиnull, если всё прошло хорошо;user?: User— результат; он необязательный, потому что при ошибке его нет.
Типы err и user в стрелочной функции TypeScript берёт из объявления done, и аннотации не нужны. Из-за user? внутри колбэка у user тип User | undefined, поэтому перед user.name стоит проверка.
API на промисах
Та же функция на промисах:
type User = { name: string };
function loadUser(): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => resolve({ name: "Anna" }), 10);
});
}
loadUser().then((user) => {
console.log(user.name); // Anna
});Здесь тип результата стоит в самом объявлении: Promise<User>. Колбэк в then получает user уже как User, без undefined и без параметра для ошибки. Сравним два способа:
- колбэк: результат описан в параметрах колбэка, а сама функция возвращает
void; - промис: функция возвращает
Promise<User>, и тип результата стоит в угловых скобках.
Этот вопрос часто задают на собеседовании: как типизировать API на колбэках и чем от него отличается API на промисах.
Ошибку тип не описывает
В Promise<User> есть место только для результата: тип ошибки там не записан. Поэтому в .catch((e) => …) параметр e получает тип any, даже в строгом режиме. В колбэке ошибка хотя бы названа в параметре err: Error | null.
Здесь аналогия с подносом перестаёт работать. В кафе одно пропавшее блюдо не отменяет остальные, а Promise.all отклоняется целиком, как только отклонится любой из промисов. Тогда await вместо подноса выбросит ошибку.
Частая ошибка
Забыли await перед Promise.all:
async function loadCount(): Promise<number> {
return 3;
}
async function main() {
const [a, b] = Promise.all([loadCount(), loadCount()]); // ✗
console.log(a + b);
}
main();error TS2488: Type 'Promise<[number, number]>' must have a '[Symbol.iterator]()' method that returns an iterator.Сообщение длинное, но в нём есть подсказка: Promise<[number, number]>. Деструктуризация [a, b] ждёт массив, а получила промис — вместо подноса у вас всё ещё чек. Нужно await Promise.all(...).
Главное
await Promise.all([...])выдаёт кортеж: тип каждой позиции берётся из промиса на той же позиции, например[User, number].- В API на колбэках результат описан в параметрах колбэка, например
(err: Error | null, user?: User) => void, а функция возвращаетvoid. - В API на промисах функция возвращает
Promise<User>, и тип результата стоит в угловых скобках. - Тип ошибки промис не описывает: в
.catch((e) => …)она приходит какany.