supermacro/neverthrow

Convenience function to convert a function that returns `Promise<Result<T,E>>` into a function that returns `ResultAsync<T,E>`

rudolfbyker opened this issue · 7 comments

I have been trying to figure this out:

/**
 * Convert a function like `(...args: A) => Promise<Result<T, E>>` into `(...args: A) => ResultAsync<T, E>`.
 *
 * Similarly to the warnings at https://github.com/supermacro/neverthrow#resultasyncfromsafepromise-static-class-method
 * you must ensure that `func` will never reject.
 */
export function wrapResultAsyncFunction<A extends any[], T, E>(
  func: (...args: A) => Promise<Result<T, E>>
): (...args: A) => ResultAsync<T, E> {
  // ???
}

This is similar to ResultAsync.fromPromise, but instead of operating on a promise, it operates on a function that returns a promise. At the same time, it should preserve the function signature.

Any ideas? If I come up with something, I'll post it here.

This was my first attempt:
image

Got it! After reading the source code for ResultAsync.fromSafePromise, I realized that new ResultAsync is also possible.

/**
 * Convert a function like `(...args: A) => Promise<Result<T, E>>` into `(...args: A) => ResultAsync<T, E>`.
 *
 * Similarly to the warnings at https://github.com/supermacro/neverthrow#resultasyncfromsafepromise-static-class-method
 * you must ensure that `func` will never reject.
 */
export function wrapResultAsyncFunction<A extends any[], T, E>(
  func: (...args: A) => Promise<Result<T, E>>
): (...args: A) => ResultAsync<T, E> {
  return (...args): ResultAsync<T, E> => new ResultAsync(func(...args));
}

Could we add something like this to the API? Maybe something like ResultAsync.fromSafeAsyncResultFunction?

Hey @rudolfbyker - my apologies for the late response.

I had meant to respond back and say the following: Rather than growing the surface area of neverthrow to handle this situation, I personally fell like the root issue is that somewhere in the code there is something creating a Promise<Result<T, E>> ... my question is; why is that type signature necessary? Why not have a ResultAsync<T, E> instead?

Due to the large delay in between my response and the previous comment, I will go ahead and close this issue. But by all means feel free to comment here if you'd like this to be re-opened.

why is that type signature necessary? Why not have a ResultAsync<T, E> instead?

  • Because of third-party code over which I have no control.
  • Because of legacy code which is slowly being migrated.
  • Because of function wrappers / decorators that take a () => T and returns () => Promise<T>. A debouncing utility would be a good example of this. (The debouncing utility may be third-party, and not aware of ResultAsync.)

There may be more use cases.

Many people in many different issues here have been asking how they can better integrate async/await with neverthrow. Most notably, people want to use await inside a function that returns ResultAsync, because it allows for much more readable code. My function above makes this possible in the most unobtrusive way that I could find so far.

Trivial example

Before

const before = (): ResultAsync<number, Error> => {
  const result1: ResultAsync<number, Error> = getSomeResultAsync1();
  const result2: ResultAsync<number, Error> = getSomeResultAsync2();

  return ResultAsync.combine([result1, result2]).map(([r1, r2]) => r1 + r2);
};

After

const after = wrapResultAsyncFunction(
  async (): Promise<Result<number, Error>> => {
    const result1: Result<number, Error> = await getSomeResultAsync1();
    const result2: Result<number, Error> = await getSomeResultAsync2();

    if (result1.isOk() && result2.isOk()) {
      return ok(result1.value + result2.value);
    }

    return err(new Error("Something went wrong"));
  }
);

Notes

  • In this trivial example, the second way uses more lines, but when using multiple combines and more complex logic than this simple addition, the second way is often much shorter, and more readable.
  • Both before and after have the same type: () => ResultAsync<number, Error>.
  • This comes with the caveat that the inner async function should not throw (as described above).

This helped me, thank you.

I had to search quite a bit to find this as it's not in the documentation (that I can find). A utility to lift some of these type to one another (or at least documentation on the use case) would be helpful.

The reasons cited above are also good IMO, my case was dealing with 3rd party code.

Curious, have folks here looked at the safeTry API? Would love to hear feedback on whether this approach with generators might work for you.

For those users that use safeTry it turns out to be their favorite part of neverthrow but I understand generators are also not the most common programming pattern in ts / js