Promise.all and callbacks
In the previous lesson there was one order and one receipt. Often there are several orders: a user and the number of their purchases come from the server separately.
Imagine you ordered soup at one window of a café and tea at another. There is no point in waiting for them one after the other: both kitchens work at the same time. It is handier to hand in both receipts at once and get a tray with both dishes on it, in the order you handed in the receipts.
Promise.all puts that tray together. It takes an array of promises and returns one promise that gives back an array of results in the same order:
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();Let's walk through what happens in main:
loadUser()andloadCount()returnPromise<User>andPromise<number>.Promise.all([...])gets both promises: both functions have already been called and run at the same time.awaitgives back the tray: an array with the user first and the number second.- The destructuring
[user, count]spreads it into variables, and the program printsAnna 3.
The tray is a tuple
The tray's type is exact. If you keep the result in one variable, const pair = await Promise.all(...), and hover over pair, the editor shows [User, number]. That is a tuple: each position has its own type.
- position 0 is
User, becauseloadUser()comes first in the array; - position 1 is
number, becauseloadCount()comes second.
That is why user.name passes the check without a single annotation. If the type were an array (number | User)[], as for an ordinary literal of two different values, you would have to check each element first. TypeScript does not mix the promises up: it takes the type of each result from the promise at the same position.
A callback API
Promises did not appear in JavaScript right away. Many older libraries and some Node.js functions report their result another way, through a callback that is called when the work is done:
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
}
});Where is the result type here? Not in the type of the function itself: loadUser returns void. The result is described in the parameters of the callback done:
err: Error | nullis the error, ornullwhen everything went well;user?: Useris the result; it is optional because there is none after an error.
TypeScript takes the types of err and user in the arrow function from the declaration of done, so no annotations are needed. Because of user?, user has the type User | undefined inside the callback, so a check stands before user.name.
A Promise API
The same function with promises:
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
});Here the result type stands in the declaration itself: Promise<User>. The callback in then gets user already as User, with no undefined and no parameter for the error. Let's compare the two ways:
- callback: the result is described in the callback's parameters, and the function itself returns
void; - promise: the function returns
Promise<User>, and the result type stands in the angle brackets.
Interviewers like to ask this: how do you type a callback API, and how does a Promise API differ from it?
No type for the error
Promise<User> has room only for the result: the error's type is not written there. That is why the parameter e in .catch((e) => …) gets the type any, even in strict mode. In a callback the error is at least named, in the parameter err: Error | null.
Here the tray analogy stops working. At a café one missing dish does not cancel the others, but Promise.all rejects as a whole as soon as any of the promises rejects. Then await throws the error instead of giving back a tray.
Common mistake
The await before Promise.all is forgotten:
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.The message is long, but it holds a hint: Promise<[number, number]>. The destructuring [a, b] expects an array but got a promise: instead of the tray you are still holding the receipt. You need await Promise.all(...).
Key points
await Promise.all([...])gives back a tuple: each position takes its type from the promise at the same position, for example[User, number].- In a callback API the result is described in the callback's parameters, for example
(err: Error | null, user?: User) => void, and the function returnsvoid. - In a Promise API the function returns
Promise<User>, and the result type stands in the angle brackets. - A promise does not describe the error's type: in
.catch((e) => …)it arrives asany.