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

safeTry with ResultAsync doesn't infer combined error types from multiple yields #603

Closed
pierback opened this issue Oct 25, 2024 · 5 comments

Comments

@pierback
Copy link

Hi, I'm using safeTry with multiple ResultAsync instances, and I think it would be beneficial if TypeScript could infer the union of all possible error types from the yielded values.

Example Code:

import { ok, err, ResultAsync, Result, safeTry } from 'neverthrow';

class MyError1 extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'MyError1';
  }
}

class MyError2 extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'MyError2';
  }
}

// Simulate an async function that may succeed or fail
function mayFail1(success: boolean): ResultAsync<number, MyError1> {
  return new ResultAsync(
    new Promise<Result<number, MyError1>>((resolve) => {
      setTimeout(() => {
        if (success) {
          resolve(ok(20));
        } else {
          resolve(err(new MyError1('Error from mayFail1')));
        }
      }, 100);
    }),
  );
}

// Simulate another async function that may succeed or fail
function mayFail2(success: boolean): ResultAsync<number, MyError2> {
  return new ResultAsync(
    new Promise<Result<number, MyError2>>((resolve) => {
      setTimeout(() => {
        if (success) {
          resolve(ok(20));
        } else {
          resolve(err(new MyError2('Error from mayFail2')));
        }
      }, 100);
    }),
  );
}

const main = () =>
  safeTry(async function* () {
    const val1Result = yield* mayFail1(true);
    const val2Result = yield* mayFail2(false);
    return ok(val1Result + val2Result);
  }).mapErr((e) => `Aborted by an error: ${e}`);

Currently, TypeScript only infers the error type from the first yielded ResultAsync (mayFail1), which means e in mapErr is inferred as MyError1. As a result, MyError2 is not recognized as a possible error type.

Ideally, TypeScript should infer the error type for mapErr as MyError1 | MyError2 to account for all possible errors that could arise from the yielded ResultAsync instances.

Is there a way to enhance safeTry to infer all possible error types without requiring explicit annotations?

@christianemmert
Copy link

It doesn't work with multiple Result types either.

@lucaschultz
Copy link

lucaschultz commented Oct 25, 2024

This is probably a regression introduced by v8.1 right? It seems to be working with 8.0 and using .safeUnwrap() in the generators.

@pierback
Copy link
Author

Does not seem to work in previous versions either. Still infers/picks the error from the first yield instead of returning a union of Errors. And to second @christianemmert, this goes for Result as well not just ResultAsync.

function mayFail1Sync(success: boolean): Result<number, MyError1> {
    if (success) {
        return ok(20)
    }

    return err(new MyError1('Error from mySyncFail1'))
}

function mayFail2Sync(success: boolean): Result<number, MyError2> {
    if (success) {
        return ok(20)
    }

    return err(new MyError2('Error from mySyncFail2'))
}

const main = () =>
    safeTry(function* () {
        const val1Result = yield* mayFail1Sync(true);
        const val2Result = yield* mayFail2Sync(false);
        return ok(val1Result + val2Result);
    }).mapErr((e) => `Aborted by an error: ${e}`);

e in mapErr is still inferred as MyError1 instead of MyError1 | MyError2

@pierback
Copy link
Author

After further investigation, I found that this is sadly a limitation of TypeScript's generator implementation: microsoft/TypeScript#57625

@janglad
Copy link

janglad commented Oct 30, 2024

Made a similar comment in #604 but leaving it here too for visibility. You can use a readonly property (or anything that differentiates the 2 types) to prevent this

import { ok, err, ResultAsync, Result, safeTry } from 'neverthrow';

class MyError1 extends Error {
  readonly name = 'MyError1';
  constructor(message: string) {
    super(message);
  }
}

class MyError2 extends Error {
  readonly name = 'MyError2';
  constructor(message: string) {
    super(message);
  }
}

// Simulate an async function that may succeed or fail
function mayFail1(success: boolean): ResultAsync<number, MyError1> {
  return new ResultAsync(
    new Promise<Result<number, MyError1>>((resolve) => {
      setTimeout(() => {
        if (success) {
          resolve(ok(20));
        } else {
          resolve(err(new MyError1('Error from mayFail1')));
        }
      }, 100);
    }),
  );
}

// Simulate another async function that may succeed or fail
function mayFail2(success: boolean): ResultAsync<number, MyError2> {
  return new ResultAsync(
    new Promise<Result<number, MyError2>>((resolve) => {
      setTimeout(() => {
        if (success) {
          resolve(ok(20));
        } else {
          resolve(err(new MyError2('Error from mayFail2')));
        }
      }, 100);
    }),
  );
}

const main = () =>
  safeTry(async function* () {
    const val1Result = yield* mayFail1(true);
    const val2Result = yield* mayFail2(false);
    return ok(val1Result + val2Result);
  }).mapErr((e) => `Aborted by an error: ${e}`);
  // (parameter) e: MyError1 | MyError2

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

4 participants