neverthrow
neverthrow copied to clipboard
orElse should accept new Ok types
Current signature:
class Result<T, E> {
orElse<A>(
callback: (error: E) => Result<T, A>
): Result<T, A> { ... }
}
Suggested signature:
class Result<T, E> {
orElse<T2, E2>(
callback: (error: E) => Result<T2, E2>
): Result<T | T2, E2> { ... }
}
Example scenario:
const getAnimal = (name: string): ResultAsync<Dog | Cat, Error> =>
getDog(name)
.orElse((err) => err instanceof NotFoundError ? getCat(name) : errAsync(err))
Current workaround:
const getAnimal = (name: string): ResultAsync<Dog | Cat, Error> =>
(getDog(name) as ResultAsync<Dog | Cat, Error>)
.orElse((err) => err instanceof NotFoundError ? getCat(name) : errAsync(err))
Here is a generalized example of a very common use case for me:
function foo(): ResultAsync<string, Error> {
return okAsync("string");
}
class FooError extends Error {}
class BarError extends Error {}
function test1() {
return foo().orElse((e) => {
if (e instanceof BarError) {
// Handle the error. Change the OK type.
return okAsync(null);
}
if (e instanceof FooError) {
// Handle the error. Keep the OK type.
return okAsync("foo");
}
// Let the error pass through.
return errAsync(e);
});
}
function test2() {
const intermediateVariable: ResultAsync<string | null, Error> = foo();
return intermediateVariable.orElse((e) => {
if (e instanceof BarError) {
// Handle the error. Change the OK type.
return okAsync(null);
}
if (e instanceof FooError) {
// Handle the error. Keep the OK type.
return okAsync("foo");
}
// Let the error pass through.
return errAsync(e);
});
}
test1 gives Type 'null' is not assignable to type 'string'., but test2 works. The only difference is an intermediate variable, where I have explicitly stated that I want to be able to change the OK type later, from string to string|null. This makes the code harder to understand, maybe even misleading, because intermediateVariable can't actually give you null.
Another workaround is to write a helper function that allows me to work with ResultAsync objects in the synchronous domain:
export function manipulateResultAsync<T1, E1, T2, E2>(
ra: ResultAsync<T1, E1>,
callback: (r: Result<T1, E1>) => Result<T2, E2>
): ResultAsync<T2, E2> {
return new ResultAsync((async () => callback(await ra))());
}
Then I can do this:
function test3() {
return manipulateResultAsync(foo(), (r) => {
if (r.isOk()) {
return r;
}
const e = r.error;
if (e instanceof BarError) {
// Handle the error. Change the OK type.
return ok(null);
}
if (e instanceof FooError) {
// Handle the error. Keep the OK type.
return ok("foo");
}
// Let the error pass through.
return err(e);
});
}
None of this is elegant.