102

How do I set the type of the rejection of my promise? Let's say I do:

const start = (): Promise<string> => {
   return new Promise((resolve, reject) => {
      if (someCondition) {
         resolve('correct!');
      } else {
         reject(-1);
      }
   });
}

Let's say I want to reject with a number. But I cannot set the type; I can pass whatever I want to the reject here.

Moreover, when using this promise, I want to have compiling error if I use the rejection response type incorrectly.

Kousha
  • 32,871
  • 51
  • 172
  • 296
  • 1
    [This issue](https://github.com/Microsoft/TypeScript/issues/7588) may be of interest. – CRice Apr 27 '18 at 22:58

7 Answers7

103

As explained in this issue, Promise doesn't have different types for fulfilled and rejected promises. reject accepts any argument that doesn't affect type of a promise.

Currently Promise cannot be typed any better. This results from the fact that a promise can be rejected by throwing inside then or catch (this is a preferable way to reject existing promise), and this cannot be handled by typing system; also, TypeScript also doesn't have exception-specific types except never.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
16

Cause there is no way to set error type in some cases like Promise, or exception throws, we can work with errors in rust-like style:

// Result<T, E> is the type used for returning and propagating errors.
// It is an sum type with the variants,
// Ok<T>, representing success and containing a value, and 
// Err<E>, representing error and containing an error value.
export type Ok<T> = { _tag: "Ok"; ok: T };
export type Err<E> = { _tag: "Err"; err: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const Result = Object.freeze({
  Ok: <T, E>(ok: T): Result<T, E> => ({ _tag: "Ok", ok }),
  Err: <T, E>(err: E): Result<T, E> => ({ _tag: "Err", err }),
});

const start = (): Promise<Result<string, number>> => {
  return new Promise((resolve) => {
    resolve(someCondition ? Result.Ok("correct!") : Result.Err(-1));
  });
};

start().then((r) => {
  switch (r._tag) {
    case "Ok": {
      console.log(`Ok { ${r.ok} }`);
      break;
    }
    case "Err": {
      console.log(`Err { ${r.err} }`);
      break;
    }
  }
});
KEIII
  • 309
  • 4
  • 9
  • 13
    I would advise against using this pattern. The problem is that you now have two ways of error handling, by checking the result's error type and by using `.catch`. You can of course assume all over your code base that only the error result will be used, but I assure you that that assumption is going to cost you in any seriously sized project. – Lodewijk Bogaards Nov 22 '20 at 00:53
  • 1
    Actually it's pattern from functional programming called Either, with all the ensuing consequences – KEIII Nov 22 '20 at 10:50
  • 2
    I know that, but that doesn't change one iota to what I have said. The assumption is what causes the problem. Also, though `Result` may be a true sum type, Promise is not a monad, so you're really not in FP land. I've been down this road far too many times and not only with TypeScript. To be honest, I should be a bit forgiving, because until somebody creates a true monadic bifunctor for async programming in TypeScript I will be cringing every time I see the word `Promise`. If god gave me a bit more time I would be that guy. – Lodewijk Bogaards Nov 23 '20 at 21:25
  • 1
    Hey @LodewijkBogaards I'd be curios to see what your thoughts are on this Async Result library I built: https://github.com/GabrielCTroia/ts-async-results. I think you make a great argument about conflicting ways of handling errors with the above pattern, hence why I opted for abstracting away the "asynchronicity" component behind a new entity – AsyncResult which just happens to use a promise underneath, but the outside world need not know this. – Gabriel C. Troia Apr 17 '21 at 23:38
  • 2
    @GabrielC.Troia haha! I recently found your library and showed it to our frontend tech lead and told him: this is how it should be done :) If you ever happen to look for a job: you're welcome to work at StackState any time! – Lodewijk Bogaards Apr 21 '21 at 14:27
  • 1
    @LodewijkBogaards Wow!! This definitely made my morning :) Thank you so much for your kind words sir. I'd be more than happy to collaborate with you guys if you ever end up using it at StackState. – Gabriel C. Troia Apr 21 '21 at 16:27
6

The exception is typed any because we cannot guarantee the correct type of the exception at design time, and neither TypeScript nor JavaScript provide the ability to guard exception types at run time. Your best option is to use type guards to provide both a design-time and run-time check in your code.

source

Mikhail Vasin
  • 2,421
  • 1
  • 24
  • 31
5

Here's my attempt at typing it:

export class ErrPromise<TSuccess, TError> extends Promise<TSuccess> {
    constructor(executor: (resolve: (value: TSuccess | PromiseLike<TSuccess>) => void, reject: (reason: TError) => void) => void) {
        super(executor);
        // Object.setPrototypeOf(this, new.target.prototype);  // restore prototype chain
    }
}

export interface ErrPromise<TSuccess, TError = unknown> {
    then<TResult1 = TSuccess, TResult2 = never>(onfulfilled?: ((value: TSuccess) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: TError) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;

    catch<TResult = never>(onrejected?: ((reason: TError) => TResult | PromiseLike<TResult>) | undefined | null): Promise<TSuccess | TResult>;
}

Use it like normal:

return new ErrPromise<T,ExecError>((resolve, reject) => { ... })

Your IDE should pick up the type of reject:

enter image description here

enter image description here

mpen
  • 272,448
  • 266
  • 850
  • 1,236
1

What @EstusFlask mentioned in his answer is correct.

But I want go one step near to an artificial solution to simulate what we want with TypeScript capabilities.

 

Sometimes I use this pattern in my codes:

interface IMyEx{
   errorId:number;
}

class MyEx implements IMyEx{
   errorId:number;
   constructor(errorId:number) {
      this.errorId = errorId;
   }
}
// -------------------------------------------------------
var prom = new Promise(function(resolve, reject) {
     try {
         if(..........)
            resolve('Huuuraaa');         
         else
            reject(new MyEx(100));
     }
     catch (error) {
            reject(new MyEx(101));
     }
});

// -------------------------------------------------------
prom()
.then(success => {
    try {
        }
    catch (error) {
        throw new MyEx(102);
    }
})
.catch(reason=>{
    const myEx = reason as IMyEx;
    if (myEx && myEx.errorId) {
       console.log('known error', myEx)
    }else{
       console.log('unknown error', reason)
    }
})
Ramin Bateni
  • 16,499
  • 9
  • 69
  • 98
1

You can use a proxy to explicitly force resolve and reject argument types. The following example does not try to mimic the constructor of a promise - because I didn't find that useful in practice. I actually wanted to be able to call .resolve(...) and .reject(...) as functions outside of the constructor. On the receiving side the naked promise is used - e.g., await p.promise.then(...).catch(...).

export type Promolve<ResT=void,RejT=Error> = {
  promise: Promise<ResT>;
  resolve: (value:ResT|PromiseLike<ResT>) => void;
  reject:(value:RejT) =>void
};

export function makePromolve<ResT=void,RejT=Error>(): Promolve<ResT,RejT> {
  let resolve: (value:ResT| PromiseLike<ResT>)=>void = (value:ResT| PromiseLike<ResT>)=>{}
  let reject: (value:RejT)=>void = (value:RejT)=>{}
  const promise = new Promise<ResT>((res,rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

The let statements look as if they are pointless - and they are pointless at runtime. But it stops compiler errors that were not easy to resolve otherwise.

(async()=>{
  const p = makePromolve<number>();
  //p.resolve("0") // compiler error
  p.resolve(0);
  // p.reject(1) // compiler error 
  p.reject(new Error('oops')); 

  // no attempt made to type the receiving end 
  // just use the named promise
  const r = await p.promise.catch(e=>e); 
})()

As shown, calls to .resolve and .reject are properly typed checked.

No attempt is made in the above to force type checking on the receiving side. I did poke around with that idea, adding on .then and .catch members, but then what should they return? If they return a Promise then it goes back to being a normal promise, so it is pointless. And it seems there is no choice but to do that. So the naked promise is used for await, .then and .catch.

Craig Hicks
  • 2,199
  • 20
  • 35
0

Here's my solution:

Define a type that extends original PromiseConstructor and Promise:

interface TypedPromiseConstructor<ResolveType, RejectType> extends PromiseConstructor {
  /**
   * Creates a new Promise.
   * @param executor A callback used to initialize the promise. This callback is passed two arguments:
   * a resolve callback used to resolve the promise with a value or the result of another promise,
   * and a reject callback used to reject the promise with a provided reason or error.
   */
  new <T = ResolveType, R = RejectType>(
    executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: R) => void) => void
  ): TypedPromise<T, R>;
}

interface TypedPromise<ResolveType, RejectType> extends Promise<ResolveType> {
  /**
   * Attaches a callback for only the rejection of the Promise.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of the callback.
   */
  catch<TResult = never>(
    onrejected?: ((reason: RejectType) => TResult | PromiseLike<TResult>) | undefined | null
  ): TypedPromise<ResolveType | TResult>;

  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise is resolved.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of which ever callback is executed.
   */
  then<TResult1 = ResolveType, TResult2 = never>(
    onfulfilled?: ((value: ResolveType) => TResult1 | PromiseLike<TResult1>) | undefined | null,
    onrejected?: ((reason: RejectType) => TResult2 | PromiseLike<TResult2>) | undefined | null
  ): Promise<TResult1 | TResult2>;
}

then convert Promise type to TypedPromiseConstructor on usage:

const promise = new (Promise as TypedPromiseConstructor<number, Error>)((resolve, reject) => {
  if (Date.now() % 2 === 0) {
    reject(new Error(`Worker stopped with exit code: 1`));
  } else {
    resolve(0);
  }
});
moontai0724
  • 184
  • 1
  • 3