Search code examples
typescriptes6-promisetypescript-typings

Typing a caught rejected promise return value


I expect this code to compile:

function promiseCatchType(): Promise<[boolean, null]>
function promiseCatchType(): Promise<[null, string]> {
  return new Promise((resolve, reject) => {
    if (Math.random() > .5) {
      return resolve();
    } else {
      return reject();
    }
  })
    .then(() => [true, null])
    .catch(() => [null, 'error']);
}

But it throws me that error:

Type 'Promise<boolean[] | [any, string]>' is not assignable to type 'Promise<[null, string]>'.
  Type 'boolean[] | [any, string]' is not assignable to type '[null, string]'.
    Type 'boolean[]' is missing the following properties from type '[null, string]': 0, 1

Here, I declare that my function can return two different kind of tuples, one in a successful case, the other on the unsuccessful case.

I want to handle my errors this way:

const [success, error] = await promiseCatchType();
if (error !== null) {
  // error is a string
  return 'An error occurred:' + error;
}

// Do something with `success`, if error is not null, then data must be of `boolean` type

Is there any way to achieve this ?


Solution

  • There are a few issues here:

    • You're representing your return values as overloads... but for overloads, the implementation's signature does not count as one of the call signatures. So the only call signature you're exposing is function promiseCatchType(): Promise<[boolean, null]>. If you want to expose two call signatures, you could write function promiseCatchType(): Promise<[boolean, null]>; function promiseCatchType(): Promise<[null, string]>; and then write a general implementation that returns something like Promise<[null, string] | [boolean, null]>. But:

    • Overloads which only differ by return type are not generally useful. There's no way for the compiler to tell which call signature promiseCatchType() should match, and in practice you'll always get the first one when you call it. Instead, I'd forget about overloads and have a single signature that returns a union.

    • Array literals like [true, null] tend to be inferred as arrays like (boolean | null)[] and not tuples. There are different ways around this but one is to explicitly assert [true, null] as [boolean, null].

    This leads me to the following code:

    function promiseCatchType(): Promise<[boolean, null] | [null, string]> {
      return new Promise((resolve, reject) => {
        if (Math.random() > .5) {
          return resolve();
        } else {
          return reject();
        }
      })
        .then(() => [true, null] as [boolean, null])
        .catch(() => [null, 'error'] as [null, string]);
    }
    

    Another issue arises when you call it. Destructuring the array into separate success and error will make the compiler lose track of the correlation between them. The compiler is generally unable to see union-type values as correlated with each other. See microsoft/TypeScript#30581 for more information. If you check that error === null, the compiler will not understand that this makes success a boolean and not a null.

    async function fooBad() {
      const [success, error] = await promiseCatchType();
      if (error !== null) {
        return 'An error occurred:' + error;
      } else {
        success; // boolean | null ?!
      }
      return;
    }
    

    Instead, you can treat the un-destructured tuple as a discriminated union, and only pull out its members after testing. That works but might not be as idiomatic as you'd like:

    async function foo() {
      const ret = await promiseCatchType();
      if (ret[1] !== null) {
        const error = ret[1];
        return 'An error occurred:' + error;
      } else {
        const success = ret[0]; // boolean
      }
      return;
    }
    

    In fact, I'd be inclined to refactor away from a tuple and to a more obvious discriminated union like {success: true, value: boolean} | {success: false, errorMessage: string} and not destructure at all. But that starts getting further afield of the question, so I'll stop there.


    Hope that helps; good luck!

    Playground link to code