11
votes

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.

3

3 Answers

5
votes

This does work!
The promise definitions were updated to support such a use case.

Runnable example is too long to be posted. But just copy/paste newest definitions and your Promise code into a an editor and you will see that your example now works.

2
votes

From the way you want to use the Promise interface it seems that you want to sometimes pass a value and sometimes you want to pass no value, so thats what optional parameters are for. If resolving a promise with 'undefined' result is not acceptable (IMO this isnt a bad idea, this tells exacly what is going on in the code - the result of promise is udefined) I can propose a solution that seems to be what you are looking for:

I would define a 'contract' for the promise, lets say:

export interface IMyPromiseResult {
    result: number;
}

and the use it with the promise:

new Promise<IMyPromiseResult>((resolve)=>{

   resolve(<IMyPromiseResult>{ result: 5 });

   resolve(<IMyPromiseResult>{ });
}

While this approach is a bit more complicated, it opens some interesting options... On the other hand - current DefinitelyTyped Promise constructor is defined as:

constructor(callback: (resolve: (result?: R) => void, reject: (error: any) => void) => void);

so having an optional result is allowed and you should be fine with it.

2
votes

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)