Skip to content

flagg2/result

Repository files navigation

@flagg2/result

npm npm npm package minimized gzipped size (select exports) NPM

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()

 static from<T, E>(fnOrThenable: (() => T | Promise<T>) | Promise<T>, errValue?: E): Promise<Result<T>>

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 errValue argument which will be the value contained in the Err variant. The origin property of the Err will be the original error that was thrown.

If the function or promise resolves successfully, the value will be wrapped in an Ok.


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.tryCatch()

Wraps a function that returns a Result but may still throw an error, in which case it is caught and wrapped in an Err with the provided error value.

A good example of when you would use this is when communicating with a database. You can have a generic error for when the communication fails, but you can also have more specific errors for constraint violations, etc.

Does not yet work with type unions as T because of weird ts behavior

 static tryCatch<T, E>(fn: () => Result<T,E> | Promise<Result<T,E>>, errValue?: E): Result<T, 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

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.origin

The original error that was thrown.

origin: Error

Err.log()

Logs the error to the console.

   log(): this

About

This library provides a Result type for Typescript, allowing for better and safer error handling.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published