- Learning TypeScript 2.x
- Remo H. Jansen
- 855字
- 2025-04-04 17:02:05
Promises
After seeing how the use of callbacks can lead to some maintainability problems, we are now going to learn about promises and how they can be used to write better asynchronous code. The core idea behind promises is that a promise represents the result of an asynchronous operation. A promise must be in one of the following three states:
- Pending: The initial state of a promise.
- Fulfilled/resolved: The state of a promise representing a successful operation. The terms "fulfilled" and "resolved" are both commonly used to refer to this state.
- Rejected: The state of a promise representing a failed operation.
Once a promise is fulfilled or rejected, its state can never change again. Let's look at the basic syntax of a promise:
function foo() { return new Promise<string>((fulfill, reject) => { try { // do something fulfill("SomeValue"); } catch (e) { reject(e); } }); } foo().then((value) => { console.log(value); }).catch((e) => { console.log(e); });
The preceding code snippet declares a function named foo that returns a promise. The promise contains a method named then, which accepts a function to be invoked when the promise is fulfilled. Promises also provide a method named catch, which is invoked when a promise is rejected.
"lib": [
"es2015.promise",
"dom",
"es5",
"es2015.generator", // new
"es2015.iterable" // new
]
We will learn more about the lib setting in Chapter 9 , Automating Your Development Workflow .
We are now going to rewrite the doSomethingAsync, doSomethingElseAsync, and doSomethingMoreAsync functions that we wrote during the callback hell example, using promises instead of callbacks:
function doSomethingAsync(arr: number[]) { return new Promise<number[]>((resolve, reject) => { setTimeout(() => { try { let n = Math.ceil(Math.random() * 100 + 1); if (n < 25) { throw new Error("n is < 25"); } resolve([...arr, n]); } catch (e) { reject(e); } }, 1000); }); } function doSomethingElseAsync(arr: number[]) { return new Promise<number[]>((resolve, reject) => { setTimeout(() => { try { let n = Math.ceil(Math.random() * 100 + 1); if (n < 25) { throw new Error("n is < 25"); } resolve([...arr, n]); } catch (e) { reject(e); } }, 1000); }); } function doSomethingMoreAsync(arr: number[]) { return new Promise<number[]>((resolve, reject) => { setTimeout(() => { try { let n = Math.ceil(Math.random() * 100 + 1); if (n < 25) { throw new Error("n is < 25"); } resolve([...arr, n]); } catch (e) { reject(e); } }, 1000); }); }
We can chain the promises that are returned by each of the preceding functions using the promises API:
doSomethingAsync([]).then((arr1) => { doSomethingElseAsync(arr1).then((arr2) => { doSomethingMoreAsync(arr2).then((arr3) => { console.log( ` doSomethingAsync: ${arr3[0]} doSomethingElseAsync: ${arr3[1]} doSomethingMoreAsync: ${arr3[2]} ` ); }); }); }).catch((e) => console.log(e));
The preceding code snippet is a little bit better than the one used in the callback example because we only needed to declare one exception handler instead of three exception handlers. This is possible because errors are propagated through the chain of promises.
The preceding example has introduced some improvements. However, the promises API allows us to chain promises in a much less verbose way:
doSomethingAsync([]) .then(doSomethingElseAsync) .then(doSomethingMoreAsync) .then((arr3) => { console.log( ` doSomethingAsync: ${arr3[0]} doSomethingElseAsync: ${arr3[1]} doSomethingMoreAsync: ${arr3[2]} ` ); }).catch((e) => console.log(e));
The preceding code is much easier to read and follow than the one used during the callback examples, but this is not the only reason to prefer promises over callbacks. Using promises also gives us better control over the execution flow of operations. Let's look at a couple of examples.
The promises API includes a method named all, which allows us to execute a series of promises in parallel and get all the results of each of the promises at once:
Promise.all([ new Promise<number>((resolve) => { setTimeout(() => resolve(1), 1000); }), new Promise<number>((resolve) => { setTimeout(() => resolve(2), 1000); }), new Promise<number>((resolve) => { setTimeout(() => resolve(3), 1000); }) ]).then((values) => { console.log(values); // [ 1 ,2, 3] });
The promises API also includes a method named race, which allows us to execute a series of promises in parallel and get the result of the first promise resolved:
Promise.race([ new Promise<number>((resolve) => { setTimeout(() => resolve(1), 3000); }), new Promise<number>((resolve) => { setTimeout(() => resolve(2), 2000); }), new Promise<number>((resolve) => { setTimeout(() => resolve(3), 1000); }) ]).then((fastest) => { console.log(fastest); // 3
});
We can use many different types of asynchronous flow control when working with promises:
- Concurrent: The tasks are executed in parallel (like in the Promise.all example)
- Race: The tasks are executed in parallel, and only the result of the fastest promise is returned
- Series: A group of tasks is executed in sequence, but the preceding tasks do not pass arguments to the next task
- Waterfall: A group of tasks is executed in sequence, and each task passes arguments to the next task (like in the example that preceded the Promise.all and Promise.race examples)
- Composite: This is any combination of the preceding concurrent, series, and waterfall approaches