Search code examples
typescriptdefinitelytyped

How do I declare a Generic Promise in Typescript, so that when the Generic type is "<void>", one of its methods won't take a parameter?


In Typescript, I want to be able to define a Promise's type in such a way so that I can do this:

//This works today:
new Promise<number>((resolve)=>{
   //Cool
   resolve(5);
   //Error, because I didn't pass a number:
   resolve();
}

//This is what I want to do also:
new Promise<void>((resolve)=>{
   //Error, because I passed a value:
   resolve(5);
   //Cool, because I declared the promise to be of type void, so resolve doesn't take a value:
   resolve();
}

The promise definition files I've seen all declare that the "resolve" method of a promise must take a value. Here is a recent example from the wonderful DefinitelyTyped project:

declare class Promise<R> implements Thenable<R> {
    constructor(callback: (resolve : (result: R) => void, reject: (error: any) => void) => void); 
///...
}

```

That basically says, "The resolve callback must be passed a value of type R." That's fine for a promise like new Promise<number>. Typescript will verify we're calling resolve with a value of type number.

However, what if I want a promise that doesn't have a value, so I want to be able to call resolve() without passing a value? I can declare my promise like this: new Promise<void> But then I'm still forced to call resolve and pass in a value of some sort. I can call resolve(undefined), but that reads a bit strangely.

There appears to be no way to properly capture this concept in Typescript: "If this generic has a type 'void', then don't expect a parameter for this function."

The closest I can do is mark the result as optional in the resolve method, but that would mean that the result is always optional, even for typed versions of Promises.


Solution

  • I might have just come up with a workaround I'm pleased with. In the case where I want to have a Promise<void> that ensures the resolve callback does not take a parameter, rather than making the resolve method always take an optional parameter, I can define a new class like so:

    export class VoidPromise extends RSVP.Promise<void>{
        //Note that resolve does not take a parameter here:
        constructor(callback:(resolve:() => void, reject:(error:any) => void) => void){
            super(callback);
        }
    }
    

    And in that case, I can use it like so:

        public static testVoidPromise() : VoidPromise{
            return new VoidPromise((resolve, reject)=>{
    
                setTimeout(1000, ()=>{
                    if (Math.random() < 0.5){
                        //Note that resolve() does NOT take a parameter
                        resolve();
                    }else{
                        reject(new Error("Something went wrong"));
                    }
                })
            });
        }
    

    Granted, devs will have to use the VoidPromise instead of simply "Promise", but the intended effect is achieved without having to falsely mark the resolve parameter as optional.

    For my scenario, the above code meets my expectations more than marking all resolve methods as having an optional result. Marking all results as optional feels dangerous in the 99% case. If it's always optional, I can declare a Promise<number>, call resolve() without a result, and get an undefined result for my promise. In that case, I should have rejected the promise. I don't believe it is expected that the resolve method's parameter is truly optional. (Source: https://github.com/domenic/promises-unwrapping#the-promise-constructor)