supermacro / neverthrow

Type-Safe Errors for JS & TypeScript
MIT License
4.03k stars 84 forks source link

PROPOSAL: Safe assignment using neverthrow #614

Open vekexasia opened 1 week ago

vekexasia commented 1 week ago

Hello all,

I started using neverthrow and enjoying it big time. Unfortunately all the attempts to have clean code when handling errors are failing big time.

I tried with safeTry but I instantly hit #604. I also tried other approaches like chaining andThen as per this comment in #301. My brain forbids me have that (although i have found brief relief in using it)..

Following the safe assignment proposal I figured that the best way would be to inherit from Go error handling (but with error first as explained in the repo).

I then created the following snippet of code that would let me call .safeRet() to produce a tuple containing the Error and the Result.


declare module "neverthrow" {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  export class Err<T, E> {
    public safeRet(): E extends never ? [E, T] : [E, undefined];
  }
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  export class Ok<T, E> {
    public safeRet(): T extends never ? [E, T] : [undefined, T];
  }
}
Ok.prototype.safeRet = function () {
  return [undefined, this.value];
};
Err.prototype.safeRet = function () {
  return [this.error, undefined];
};

What happens is the following:


const [e, s] = ok("string").safeRet();
// e: undefined, s: string
const [e2, s2] = err("error").safeRet();
// e2: "error", s2: undefined

function a(x: Result<number, string>): Result<number, string> {
  const [e3, s3] = x.safeRet();
  // e3: string | undefined, s3: number | undefined
  // this forces you to check for e3
  if (typeof e3 !== "undefined") {
    return err(e3);
  }
  // s3 is now number
  return ok(s3);
}

NOTE: when using ok we can directly use the result as typescript properly infer that s is always set

If by any case we change from Result<*, never> to an Error, then automatically typescript infers the result to be T | undefined forcing you to either use the Non-null assertion operator (!) or check for error like in the a function above.

just to give you perspective of what i mean this is what a chain of neverthrow "compatible" functions might look like.


declare function randomNumber(): Ok<number, never>;
declare function safeDivision(
  a: number,
  b: number,
): Result<number, "cannot divide by zero">;
declare function errIfAbove1(
  what: number,
): Result<undefined, "CannotBe>1" | undefined>;

function randomDivision(): Result<
  number,
  "cannot divide by zero" | "CannotBe>1"
> {
  const [, firstOperand] = randomNumber().safeRet();
  const [, secondOperand] = randomNumber().safeRet();

  const [divErr, divRes] = safeDivision(firstOperand, secondOperand).safeRet();

  if (typeof divErr !== "undefined") {
    return err(divErr);
  }

  const [above1Err] = errIfAbove1(divRes).safeRet();
  if (typeof above1Err !== "undefined") {
    return err(above1Err);
  }

  return ok(divRes);
}

For now i am monkey patching like shown above. I was thinking of writing a small library but I thought I should first stop by here and see if maybe this could come handy and get included in a future neverthrow version.