1

I have a JavaScript codebase with several examples of the following (playground link here):

interface ComplexObject {
    field1: string;
    field2: boolean;
    field3: number;
}
declare function getField1(): Promise<string>;
declare function getField2(): Promise<boolean>;
declare function getField3(): Promise<number>;
const promiseBuilder = function() : Promise<ComplexObject> {
    //Not using Partial here results in a ts(2739) error that
    //field1, field2, field3 are missing from type '{}'.
    const result: Partial<ComplexObject> = {}
    return new Promise(function (resolve, reject) {
        getField1().then(function(field1){
            result.field1 = field1;
            return getField2();
        }).then(function(field2){
            result.field2 = field2;
            if(field2) {
                return getField3();
            } else {
                return Promise.resolve(0);
            }
        }).then(function(field3){
            result.field3 = field3;
            //result now has all the fields, but there's an error:
            //Partial<ComplexObject>' is not assignable to 'ComplexObject'.
            resolve(result); 
        }).catch(function(err) {
            reject(err);
        });
    })
}

Without type annotations, this works just fine in JavaScript, but in TypeScript it has the errors indicated.

What is the canonical way to do something like this in TypeScript?

WBT
  • 2,249
  • 3
  • 28
  • 40
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi May 04 '22 at 23:29
  • @Bergi if it were just one field, I'd agree that the extra steps are superfluous, but there is nontrivial code logic in bringing things together before the final resolve, and it is using chaining. – WBT May 04 '22 at 23:53
  • I'm not talking about the chaining, I'm talking about that final `resolve` call. It shouldn't be there at all, it should just be `return result`, and you should remove the `new Promise(function (resolve, reject) {` wrapper. – Bergi May 05 '22 at 01:30

1 Answers1

1

The closest found so far requires converting to the async style, using local consts to hold the values temporarily, and assembling the entire object at once, like this (playground link here):

const asyncBuilder = async function() : Promise<ComplexObject> {
    const field1 = await getField1();
    const field2 = await getField2();
    let field3 = 0;
    if(field2) {
        field3 = await getField3();
    }
    return { field1, field2, field3 };
}

However, that means a lot of rewriting what was previously valid code, so if there's a way to do it with less code rewriting, that would be good to know about!

WBT
  • 2,249
  • 3
  • 28
  • 40
  • This rewriting will be worth it, regardless whether you do it because type annotations are complicated otherwise, or do it because it's just cleaner, shorter, more readable code. – Bergi May 04 '22 at 23:31
  • If you don't want to do the rewriting, just assert `result as ComplexObject`. That's how the JS code was intended to work. Yes, it's sloppy. – Bergi May 04 '22 at 23:34
  • I'd even go as far as `const field3 = field2 ? await getField3() : 0;` – Bergi May 04 '22 at 23:35
  • @Bergi sure, but the real logic is way more complex than a ternary operator can capture. – WBT May 04 '22 at 23:50