supermacro / neverthrow

Type-Safe Errors for JS & TypeScript
MIT License
3.9k stars 80 forks source link

Mentioned `safeTry` Error Inference Limitations in Docs #604

Open lucaschultz opened 1 week ago

lucaschultz commented 1 week ago

In #603, @pierback mentioned that only the first yield is considered in the safeTry type inference due to a TypeScript limitation (see https://github.com/microsoft/TypeScript/issues/57625). This is a significant drawback of using safeTry and should be noted in the documentation, as it can easily lead to bugs due to incomplete error handling. I'll have to check my codebases now and see wether this is an issue in my code 😌

Perhaps neverthrow could include an example like this:

class MyError1 extends Error {}
class MyError2 extends Error {}

declare function mayFail1(): Result<number, MyError1>
declare function mayFail2(): Result<number, MyError2>

// Return type is Result<number | null, MyError1 | MyError2>
function myFunc() { 
  return safeTry<number | null, MyError1 | MyError2>(function* () {
    const value1 = yield* mayFail1()
    const value2 = yield* mayFail2()

    const result = value1 + value2

    if (result < 0) {
      return ok(null)
    }

    return ok(result)
  })
}

// Inferred return type is Result<number | null, MyError1>
function myInferredFunc() { 
  return safeTry(function* () {
    const value1 = yield* mayFail1()
    const value2 = yield* mayFail2()

    const result = value1 + value2

    if (result < 0) {
      return ok(null)
    }

    return ok(result)
  })
}

Including this in the safeTry documentation would illustrate both the issue and the expected inference of ok values. The documentation should also emphasize that when using safeTry, developers must always specify all potential errors as generics.

janglad commented 1 week ago

FYI, you can use a tag (or really any property that differentiates the 2 types) to prevent this. I would suggest instead updating the documentation to recommend this approach.


import { ok, Result, safeTry } from "neverthrow";
class MyError1 extends Error {
  readonly _tag = "MyError1";
}
class MyError2 extends Error {
  readonly _tag = "MyError2";
}

declare function mayFail1(): Result<number, MyError1>;
declare function mayFail2(): Result<number, MyError2>;

const test = safeTry(function* () {
  yield* mayFail1();
  yield* mayFail2();
  return ok("Hello");
});

// const test: Result<string, MyError1 | MyError2>