@flagg2/result
v1.8.2
Published
A type-safe rust-like result type for TypeScript
Downloads
469
Maintainers
Readme
@flagg2/result
This library provides a Result type for Typescript, allowing for better and safer error handling.
Table of Contents
Features
- Rust-like Result type
- Better error handling
- Automatic type inference
- More robust code
- Zero dependencies
- Minimalistic
- Small package size
Usage
Imagine having a function which you use to split time into seconds and minutes. We will look at one implementation which uses result and a second one which does not.
// returns {hours: number, mins: number}
function parseTime(time: string) {
const splitTime = time.split(":")
return {
hours: parseInt(splitTime[0], 10),
mins: parseInt(splitTime[1], 10),
}
}
Now you call parseTime
in a different place of our codebase.
This function uses a .split
and relies on the result being at least 2 items long.
Because of that, you have to keep in mind that this function could throw, even though there is no indication by the type system that it could be the case.
This leads to uncaught errors.
Somehow, the function get called with an incorrect argument, for example "2051" instead of "20:51".
This arugment is however still a string which makes typescript unable to help us catch this error.
function faultyArgument() {
const time = "2051"
const result = splitTime(time)
// You do not have any indication by the type system that this could throw.
// You forget to use a try catch segment and end up with a runtime error
return result
}
This is when the Result
class comes in. Result indicates a computation which could fail. At runtime, could be either an Ok
or an Err
depending on cirumstances.
The massive benefit we get with Result
is that we do not catch errors like the previously mentioned one at runtime , but rather at compilation time .
Let's look at the previous example with Result
function parseTime(time: string) {
const splitTime = time.split(":")
if (splitTime.length !== 2) {
return Result.err("SPLIT_ERROR")
}
if (isNaN(parseInt(splitTime[0], 10)) || isNaN(parseInt(splitTime[1], 10))) {
return Result.err("PARSE_ERROR")
}
if (parseInt(splitTime[0], 10) > 23 || parseInt(splitTime[1], 10) > 59) {
return Result.err("VALUE_ERROR")
}
return Result.ok({
hours: parseInt(splitTime[0], 10),
mins: parseInt(splitTime[1], 10),
})
}
Now, using the Result pattern, we are forced to deal with the fact that it could fail at compilation time .
Better yet, we know exactly which errors can occur and we can handle them accordingly.
For example:
function faultyArgument() {
const time = "2051"
const result = parseTime(time)
// type is Result<{hours: number, mins: number}, "SPLIT_ERROR" | "PARSE_ERROR" | "VALUE_ERROR">
// Here you gracefully handle the error case
if (result.isErr()) {
// errValue is only available after the type system is sure that the result is an Err
switch (result.errValue) {
case "SPLIT_ERROR":
console.log("The time was not in the correct format")
break
case "PARSE_ERROR":
console.log("The time contained non-numeric characters")
break
case "VALUE_ERROR":
console.log("The time contained invalid values")
break
}
return
}
// Here the type system is sure that the result is an Ok, and we get access to the "value" property
const { hours, mins } = result.value
console.log(`The time is ${hours}:${mins}`)
}
As you can see, it is much harder to shoot yourself in the foot while handling errors, making our code much more robust.
Whenever possible, the result return type gets inferred automatically for the best dev experience possible.
Base Classes
Result<T, E>
A class representing a computation which may succeed or fail.
Ok<T>
A class representing a successful computation.
Err<E>
A class representing a failed computation.
API
Result
Result.ok()
Creates a new Ok
variant; If no value is provided, it defaults to null
.
static ok<T>(value?: T): Ok<T>
Result.err()
Creates a new Err
variant. If no value is provided, it defaults to null
. Optionally takes an origin
argument which is the original error that was thrown.
static err<E>(errValue?: E, origin?: Error): Err<E>
Result.from()
Creates a Result
from a function, a promise, or a promise-returning function.
If an error is thrown at any point, it is caught and wrapped in an Err
. Takes an optional catchFn
argument which should be a function returning the value contained in the Err
variant.
If the function or promise resolves successfully, the value will be wrapped in an Ok
.
static from<T, E>(fnOrPromise: (() => T | Promise<T>) | Promise<T>, catchFn = (err: Error) => null as E): Promise<Result<T>>
Result.tryCatch()
Wraps a function that returns a Result
but may still throw an error, in which case it is caught and wrapped in and Err
from the catchFn.
static tryCatch<T, const E = null>(fn: () => Result<T, E> | Promise<Result<T, E>>,catchFn: (err: Error) => E = () => null as E)
Result.infer()
Sometimes type inference does not work well with Result
unions. You might notice that your arguments are being inferred as any
or that the return types are not correct.
This can be the case when using andThen , map , mapErr , or match .
When this happens, call this function to get a type that is easier to work with.
static infer<T extends Result>(result: T): T
Result.all()
Takes an array of Result
instances and returns a single Result
instance containing an array of all the Ok
values.
If any of the results are an Err
, the first Err
value is returned.
static all<T>(results: Result<T>[]): Result<[...]>
Result.isOk()
Returns true
if the result is an Ok
variant. If true, casts the result as Ok
isOk(): this is Ok<T>
Result.isErr()
Returns true
if the result is an Err
variant. If true, casts the result as Err
isErr(): this is Err<E>
Result.unwrap()
Returns the contained Ok
value. Throws an error if the value is an Err
.
unwrap(): T
Result.unwrapErr()
Returns the contained Err
value. Throws an error if the value is an Ok
.
unwrapErr(): E
Result.unwrapOr()
Returns the contained Ok
value. If the value is an Err
, returns the provided default value.
unwrapOr(defaultValue: T): T
Result.expect()
Returns the contained Ok
value. If the value is an Err
, throws an error with the provided message.
expect(message: string): T
Result.expectErr()
Returns the contained Err
value. If the value is an Ok
, throws an error with the provided message.
expectErr(message: string): E
Result.match()
Calls the appropriate function based on the result based on if it is an Ok
or an Err
.
match<U>(fn: { ok: (value: T) => U; err: (errValue: E) => U }): U
Result.andThen()
Calls the provided function if the result is an Ok
. If the result is an Err
, returns the Err
value.
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E>
Result.map()
Maps a Result<T, E>
to Result<U, E>
by applying a function to a contained Ok
value, leaving an Err
value untouched.
map<U>(fn: (value: T) => U): Result<U, E>
Result.mapErr()
Maps a Result<T, E>
to Result<T, F>
by applying a function to a contained Err
value, leaving an Ok
value untouched.
mapErr<F>(fn: (errValue: E) => F): Result<T, F>
Result.logIfErr()
Logs the error to the console if the result is an Err
.
logIfErr(): this
Ok
Ok.value
The value contained in the Ok
variant.
value: T
Err
Err.errValue
The value contained in the Err
variant.
errValue: E
Err.cause
A chain of Result instances that led to the error. Might have a Error instance at the end of the chain.
origin: Cause
Err.log()
Logs the error to the console.
log(): this
Err.getTrace(): string
Returns a string representation of the error chain.
getTrace(): string
Err.getCause(): Cause
Returns the cause of the error.
getCause(): Cause
Err.getRootCause(): Cause
Returns the root cause of the error.
getRootCause(): Cause