Rust-like Result for TypeScript
Node Package 📦
- 100% TypeScript and ESM
- No external dependencies
- Similar to Rust
wrap()
/unwrap()
/rewrap()
conversion (async/sync)ok()
/err()
for converting to Option from@hazae41/option
(with optional chaining?.
)isOk()
/isErr()
type guardsmap()
/tryMap()
mapping (async/sync)unwrapOr()
default value
When designing a function, you never know how to return that the action failed
This is the standard way of dealing with errors
But you are forced to try-catch, you also need to be aware that the function may throw
// does this throw? I don't knowfunctiondoSomething():stringtry{constresult=doSomething()// use result}catch(e:unknown){// use e (you don't know what it is)}
And the error is not typed, so you often end up checking if that's an error, and if it is not, you don't know what to do
try{// ...}catch(e:unknown){if(einstanceofError)// use eelse// what should I do now? rethrow?}
The advantage is that the error is explicit (you know it can fail) and typed
But you have to check forinstanceof Error
each time
functiondoSomething():string|Errorconstresult=doSomething()if(resultinstanceofError)throwresult// use result
The advantage is that you can use optional chaining?.
functiondoSomething():string|undefinedconstmaybeSlice=doSomething()?.slice(0,5)
But if you want to throw, you have to explicitly check forundefined
, and the "burden of naming the error" is on you instead of the function you used
functiondoSomething():string|undefinedconstresult=doSomething()if(result===undefined)thrownewError(`something failed, idk`)// use result
Andundefined
may mean something else, for example, a function that reads from IndexedDB:
functionread<T>(key:string):T|undefined
Doesundefined
mean that the read failed? Or does it mean that the key doesn't exist?
This is the way
It's a simple object that allows you to do all of the methods above, and even more:
- Throw with
unwrap()
- Get the data and error with
ok()
anderr()
, with support for optional chaining?.
- Check the data and error with
isOk()
andisErr()
type guards - Map the data and error with
map()
andmapErr()
- Use a default value with
unwrapOr()
Useunwrap()
to get the inner data if Ok or throw the inner error if Err
import{Result,Ok,Err}from"@hazae41/result"functionunwrapAndIncrement(result:Result<number>):number{returnresult.unwrap()+1}unwrapAndIncrement(Ok.new(0))// will return 1unwrapAndIncrement(Err.error("Error")))// will throw Error("Error")
Useok()
anderr()
to get an Option, and useinner
to get the inner value ifSome
, orundefined
ifNone
functionmaybeSlice(result:Result<string>):string|undefined{returnresult.ok().inner?.slice(0,5)}maybeSlice(newOk("hello world"))// will return "hello"maybeSlice(Err.error("Error"))// will return undefined
You can easily map inner data if Ok and do nothing if Err, with support for async and sync
import{Result,Ok,Err}from"@hazae41/result"functiontryIncrement(result:Result<number,Error>):Result<number,Error>{returnresult.mapSync(x=>x+1)}tryIncrement(newOk(0))// Ok(1)tryIncrement(Err.error("Error"))// Err(Error("Error"))
You can easily check for Ok or Err and it's fully type safe
import{Result,Ok,Err}from"@hazae41/result"functionincrementOrFail(result:Result<number,Error>):number|Error{if(result.isOk())result// Ok<number>elseresult// Err<Error>}
You can easily wrap try-catch patterns, with support for async and sync
constresult=Result.runAndWrapSync(()=>{if(something)return12345elsethrownewError("It failed")})
If another library implements its own Result type, as long as it hasunwrap()
, you can rewrap it to this library in one function
interfaceOtherResult<T>{unwrap():T}functionrewrapAndIncrement(other:OtherResult<number>):Result<number>{returnResult.rewrap(other).mapSync(x=>x+1)}
When using Result, throwing is seen as "panicking", if something is thrown and is not expected, it should stop the software
So the try-catch pattern is prohibited in Result kingdom, unless you use external code from a library that doesn't use Result
try{returnnewOk(doSomethingThatThrows())}catch(e:unknown){returnnewErr(easError)}
But, sometimes, you want to do a bunch of actions, unwrap everything, catch everyting and return Err
/** * BAD EXAMPLE **/try{constx=tryDoSomething().unwrap()consty=tryDoSomething().unwrap()constz=tryDoSomething().unwrap()returnnewOk(doSomethingThatThrows(x,y,z))}catch(e:unknown){returnnewErr(easError)}
But what if you only want to catch errors thrown fromErr.unwrap()
, and not errors coming fromdoSomethingThatThrows()
?
You can do so by usingResult.unthrow()
, it will do a try-catch but only catch errors coming fromErr.throw()
returnResult.unthrowSync<void,Error>(t=>{constx=tryDoSomething().throw(t)consty=tryDoSomething().throw(t)constz=tryDoSomething().throw(t)returnnewOk(doSomethingThatThrows(x,y,z))})