jsjslim jsjslim - 4 months ago 61
TypeScript Question

Chaining Promises in Typescript

How do I convert async/await code

(Typescript + es6 target)
to using chained
Promise.then()
?

For example:

function mockDelay<T>(getValue:()=>T): Promise<T> {
return new Promise<T>(resolve=>setTimeout(()=>resolve(getValue()), 10));
}
// Assume blackbox implementation
class Service {
constructor(private i=1, private callCount=0){}
opA() : Promise<number> {
this.callCount++;
return mockDelay(()=>this.i+=1);
}
opB(value:number) : Promise<number> {
this.callCount++;
return mockDelay(()=>this.i+=value);
}

opC(): Promise<number> {
return mockDelay(()=>this.i+=2);
}

isA(): Promise<boolean> { return mockDelay(()=>this.callCount%2===0); }
isC(): Promise<boolean> { return mockDelay(() =>true); }
}

// Convert this async/await code to using chained Promises
async function asyncVersion(): Promise<string[]>{
let expected:string[] = [];
let lib = new Service();
let sum = 20;
let lastValue = 0;
while (sum > 0) {
expected.push(`${sum} left`);
if (await lib.isA())
{
expected.push("Do A()");
lastValue = await lib.opA();
sum -= lastValue;
}
else
{
expected.push("Do B()");
lastValue = await lib.opB(lastValue);
sum -= lastValue*3;
if (await lib.isC()) {
expected.push("Do C()");
sum += await lib.opC();
}
}
}
expected.push("All completed!");
return expected;
};

function chainPromiseVersion(): Promise<string[]>{
// How to convert the asyncVersion() to using chained promises?
return Promise.resolve([]);
}

// Compare results
// Currently running asyncVersion() twice to ensure call results are consistent/no side effects
// Replace asyncVersion() with chainPromiseVersion()
Promise.all([asyncVersion(), asyncVersion() /*chainPromiseVersion()*/])
.then(result =>{
let expected = result[0];
let actual = result[1];
if (expected.length !== actual.length)
throw new Error(`Length: expected ${expected.length} but was ${actual.length}`);
for(let i=0; i<expected.length; i++) {
if (expected[i] !== actual[i]){
throw new Error(`Expected ${expected[i]} but was ${actual[i]}`);
}
}
})
.then(()=>console.log("Test completed"))
.catch(e => console.log("Error: " + e));


I know I can transpile es6 code to es5 using Babel (Github example).

This question is about manually rewriting async/await code to using chained promises.

I can convert simple examples like the following.

// Async/Await
(async function(){
for (let i=0; i<5; i++){
let result = await mockDelay(()=>"Done " + i);
console.log(result);
}
console.log("All done");
})();

// Chained Promises
(function(){
let chain = Promise.resolve(null);
for (let i=0; i<5; i++){
chain = chain
.then(()=>mockDelay(()=>"Done " + i))
.then(result => console.log(result));
}
chain.then(()=>console.log("All done"));
})();


But have no idea how to convert the example above, where:


  • loop condition affected by promise result

  • execution must be one-after-the-other (no
    Promise.all()
    )


Answer

awaits become then calls - often nested ones for scoping and control flow to be effective - and loops become recursion. In your case:

(function loop(lib, sum, lastValue){
    if (sum > 0) {
        console.log(`${sum} left`);
        return lib.isA().then(res => {
            if (res) {
                console.log("Do A()");
                return lib.opA().then(lastValue => {
                    sum -= lastValue;
                    return loop(lib, sum, lastValue);
                });
            } else {
                console.log("Do B()");
                return lib.opB(lastValue).then(lastValue => {
                    sum -= lastValue*3;
                    return lib.isC().then(res => {
                        if (res) {
                            console.log("Do C()");
                            return lib.opC().then(res => {
                                sum += res;
                                return loop(lib, sum, lastValue);
                            });
                        }
                        return loop(lib, sum, lastValue);
                    });
                });
            }
        });
    } else {
        console.log("All completed!");
        return Promise.resolve()
    }
})(new Service(), 20, 0);

Luckily you had no form of break/continue/return inside your loop, as that would've made it even more complicated. In general, convert all statements to continuation passing style, then you can defer them where necessary.