supermacro / neverthrow

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

[Feature request] have `.unwrapOr()` equivalent that runs a function #587

Closed janglad closed 4 weeks ago

janglad commented 1 month ago

Title, probably best to implement it as a new method as this could be breaking for people that are currently returning functions from .unwrapOr().

Example usecase specific usecase for me


declare const getSignedInUser: () => ResultAsync<User, AuthError> 

import { redirect } from "next/navigation";
const redirectToAuth = () => {
 // Throws under the hood so has returntype of never
    redirect("/login")
}

// On page
const user = await getSignedInUser().unwrapOr(redirectToAuth)

but a more common use case would be having a dynamic value as a fallback (result.error could be provided as argument) or one that you want to compute lazily.

If this is something other people want please leave a suggestion for naming and I can probably implement this.

paduc commented 1 month ago

Hello @janglad

I believe match can handle your use case.

const user = await getSignedInUser().match( (user) => user, (e) => { redirectToAuth() }); 
mattpocock commented 1 month ago

@janglad mapErr does this, I think

janglad commented 1 month ago

@mattpocock not quite, still leaves you with a Result

interface User {
  id: string;
}

interface AuthError {
  code: string;
}

declare const getUser: () => ResultAsync<User, AuthError>;
declare const redirect: () => never;

const user = await getUser().mapErr(redirect);
//  Result<User,never>

const user2 = await getUser().match((user) => user, redirect);
//    User

while not super on topic for this issue this does make me wonder if a Result<T, never> should be typed as an Ok<T, never>? The Ok would have .value available right away without checking for isOk(), and the never already indicates that there can't be any error case. Altho no idea if this is actually implemented differently and might cause issues.

@paduc that's true, but while obv a quick val => val isn't the biggest deal it's also not the most elegant.

paduc commented 1 month ago

@janglad I believe what you are trying to do may be outside the scope of this lib, which is handling error types. You are trying to add behavior / actions.

There are plenty of ways to achieve this without adding to the API.

janglad commented 1 month ago

@paduc yea agree that redirecting is def out of scope, but not sure if using a function to get the fallback is? Similar to how on Java optionals you have orElse(val) and orElseGet(() -> val). But I guess that it's not a very common use-case seeing how it hasn't come up before.

iyefrat commented 1 month ago

I'm not sure if emulating Rust's result module in it's entirety is a goal here, but if so, they have a unwrapOrElse method that does exactly this https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else

supermacro commented 4 weeks ago

I'm not sure if emulating Rust's result module in it's entirety is a goal here

Correct.

https://gdelgado.ca/neverthrow-subtyping#title


Also why does match not meet your needs?

iyefrat commented 3 weeks ago

Also why does match not meet your needs?

unwrapOr is also redundant with match`, it's just a clearer declaration of intent I think:

result.match((x) => x, () => 'an error')
result.unwrapOr('an error')
// similarly
result.match((x) => x, (error) => JSON.stringify(error))
result.unwrapOrElse((error) => JSON.stringify(error))
paduc commented 3 weeks ago

@iyefrat I agree that unwrapOr is also syntactic sugar but just a nitpick: it is used to pass a default value not an error.

iyefrat commented 3 weeks ago

oh sure, I just didn't really have a default value to use as a generic example, but you're right that a better sketch would be:

result.match((x) => x, () => 'default')
result.unwrapOr('default')
// similarly
result.match((x) => x, (error) => recoverFromError(error))
result.unwrapOrElse((error) => recoverFromError(error))