Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better type inference when not chaining (better interop with async/await) #514

Open
bvisch opened this issue Nov 7, 2023 · 5 comments
Open

Comments

@bvisch
Copy link

bvisch commented Nov 7, 2023

This library is really helping us at work to make our code safer. However, we've noticed that readability can suffer when using .andThen/.orElse chains, especially when integrating with old async code that doesn't use neverthrow. On the other hand, if we don't chain functions, we get very unreadable return types.

Here's something we came up with to make migration easier (demo playground here):

export type ResultError<T> = T extends Result<infer _, infer E>
  ? E
  : T extends ResultAsync<infer _, infer E>
  ? E
  : never;

export type ResultValue<T> = T extends Result<infer V, infer _>
  ? V
  : T extends ResultAsync<infer V, infer _>
  ? V
  : never;

/**
 * Takes a function that returns a Promise<Result<>> and transforms its return type to ResultAsync<>.
 * Still works if fn returns something like Promise<Result<A, B> | Err<never, C>> - transforms return type to ResultAsync<A, B | C>
 */
export function toResultAsyncFn<
  TArgs extends any[],
  R extends Result<any, any>,
>(
  fn: (...args: [...TArgs]) => Promise<R>,
): (...args: [...TArgs]) => ResultAsync<ResultValue<R>, ResultError<R>> {
  return (...args: Parameters<typeof fn>) =>
    ResultAsync.fromSafePromise(fn(...args)).andThen((result) => {
      if (result.isOk()) return okAsync(result.value);
      else return errAsync(result.error);
    });
}

/**
 * Takes a function that returns a Promise<Result<>>, evaluates it, and returns a ResultAsync<>.
 * Still works if fn returns something like Promise<Result<A, B> | Err<never, C>> - returns a ResultAsync<A, B | C>
 */
export function makeResultAsync<R extends Result<any, any>>(
  fn: () => Promise<R>,
): ResultAsync<ResultValue<R>, ResultError<R>> {
  return ResultAsync.fromSafePromise(fn()).andThen((result) => {
    if (result.isOk()) return okAsync(result.value);
    else return errAsync(result.error);
  });
}

And here's what they look like in use:

// returns Promise<Ok<"ok", never> | Err<never, 1> | Ok<2, never> | Err<never, "err">>
async function returnsPromisedResult(str: Promise<string>) {
  const s = await str;
  if (s.length === 0) return ok("ok" as const);
  else if (s.length === 1) return err(1 as const);
  else if (s.length === 2) return ok(2 as const);
  else return err("err" as const);
}

// returns ResultAsync<"ok" | 2, 1 | "err">
function returnsResultAsync(str: string) {
  return makeResultAsync(async () => {
    const s = await Promise.resolve(str);
    if (s.length === 0) return ok("ok" as const);
    else if (s.length === 1) return err(1 as const);
    else if (s.length === 2) return ok(2 as const);
    else return err("err" as const);
  });
}

// (str: Promise<string>) => ResultAsync<"ok" | 2, 1 | "err">
const returnsResultAsync2 = toResultAsyncFn(returnsPromisedResult);

These functions would have saved us a lot of frustration if they were available to us from the start. Are there any issues with adding them to the library? I'm happy to do it if I can get sign-off. 🙂

@bvisch bvisch changed the title Better type inference when not chaining Better type inference when not chaining (better interop with async/await) Nov 7, 2023
@breakpoint-2023
Copy link

Looks awesome. Totally feel like this is something that is needed.

@paduc
Copy link
Contributor

paduc commented Nov 28, 2023

I might be missing something but what does this achieve that .match() can’t ?

@abranhe
Copy link

abranhe commented Mar 14, 2024

This would be a selling point because it allows the seamless integration of async/await code.

@harutyundr
Copy link

I had a problem with methods not correctly infering the value type on returned ResultAsync. In the following code, the type of result variables should have been the same after narrowing down with result.isErr() check, but's not the case:

const promise1 = async () => {
	const result = Math.random() > 0.5 ? ok(1) : err('error');
	if (result.isErr()) {
		return result; // const result: Err<never, string>
	}

	return ok('String only result');
};
const promise2 = async () => {
	const result = Math.random() > 0.5 ? await okAsync(1) : await errAsync('error');
	if (result.isErr()) {
		return result; // const result: Err<never, string> | Err<number, never>
	}
	return ok('String only result');
};

Even though type Result<T, E> = Ok<T, E> | Err<T, E> it seems returning A PromiseLike<Result<T, E>> breaks narrowing-down of the type. This became apparent to me when I tried to turn the Promise into a ResultAsync using the code from @bvisch

I have fixed it with replacing Result<T, E> with Ok<T, never> | Err<never, E> everywhere in ResultAsync types:

diff --git a/node_modules/neverthrow/dist/index.d.ts b/node_modules/neverthrow/dist/index.d.ts
index e542ecf..bd32b72 100644
--- a/node_modules/neverthrow/dist/index.d.ts
+++ b/node_modules/neverthrow/dist/index.d.ts
@@ -2,9 +2,9 @@ interface ErrorConfig {
     withStackTrace: boolean;
 }
 
-declare class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
+declare class ResultAsync<T, E> implements PromiseLike<Ok<T, never> | Err<never, E>> {
     private _promise;
-    constructor(res: Promise<Result<T, E>>);
+    constructor(res: Promise<Ok<T, never> | Err<never, E>>);
     static fromSafePromise<T, E = never>(promise: PromiseLike<T>): ResultAsync<T, E>;
     static fromPromise<T, E>(promise: PromiseLike<T>, errorFn: (e: unknown) => E): ResultAsync<T, E>;
     static combine<T extends readonly [ResultAsync<unknown, unknown>, ...ResultAsync<unknown, unknown>[]]>(asyncResultList: T): CombineResultAsyncs<T>;
@@ -25,7 +25,7 @@ declare class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
      * Emulates Rust's `?` operator in `safeTry`'s body. See also `safeTry`.
      */
     safeUnwrap(): AsyncGenerator<Err<never, E>, T>;
-    then<A, B>(successCallback?: (res: Result<T, E>) => A | PromiseLike<A>, failureCallback?: (reason: unknown) => B | PromiseLike<B>): PromiseLike<A | B>;
+    then<A, B>(successCallback?: (res: Ok<T, never> | Err<never, E>) => A | PromiseLike<A>, failureCallback?: (reason: unknown) => B | PromiseLike<B>): PromiseLike<A | B>;
 }
 declare const okAsync: <T, E = never>(value: T) => ResultAsync<T, E>;
 declare const errAsync: <T = never, E = unknown>(err: E) => ResultAsync<T, E>;

@lucaschultz
Copy link

lucaschultz commented Oct 25, 2024

I second this. I have pretty much exactly the same code as @bvisch posted here in each project that uses neverthrow. Having them available from the package, well tested and with good type inference, would be wonderful. My version uses the ResultAsync constructor, though:

import { Result, ResultAsync } from 'neverthrow'

export type AnyResult = Result<any, any>

export type InferResultError<T> =
  T extends Result<infer _, infer E>
    ? E
    : T extends ResultAsync<infer _, infer E>
      ? E
      : never
      
export type InferResultValue<T> =
  T extends Result<infer V, infer _>
    ? V
    : T extends ResultAsync<infer V, infer _>
      ? V
      : never

export function resultFromSafeAsyncFn<T extends AnyResult>(
  fn: () => Promise<T>,
) {
  return new ResultAsync(fn()) as ResultAsync<
    InferResultValue<T>,
    InferResultError<T>
  >
}

@bvisch have you considered this approach and found any downsides to it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants