iron-oxide

iron-oxide offers some Rust style exception handling and rudimentary pattern matching.

The Problem

Exception handling in JavaScript and TypeScript can be notoriously cumbersome and confusing. For example, Array.prototype.find returns undefined and Array.prototype.indexOf returns -1; the return types for these exceptions don't match and thus can be confusing, especially for newer developers. JS also allows you to throw errors and catch them using try/catch blocks. The problem compounds itself even more when we start using promises and asynchronous code.

The Solution

In Rust, we have two types in the standard library Option and Result for handling exceptions. Option is used to catch failure in a part of the application where it doesn't make sense to panic, or throw in the case of JavaScript. Result is similar to Option except that it can contain the reason for the failure, allowing us to say why a function failed.

Option comes in two flavors:

  • Some representing success or a value
  • None representing failure or lack of a value

Result also comes in two flavors:

  • Ok representing success or a value
  • Err representing failure, or lack of a value, with a reason

Pattern Matching

Rust provides pattern matching through the use of the match keyword. iron-oxide attempts to replicate this by including a match function.

Installation

This project is distributed via npm which is bundled with node and should be installed as one of your project's dependencies.

$ npm install iron-oxide

ℹ️ iron-oxide is written in TypeScript and comes bundled with type declarations

Using Option

Option is used to catch errors in a uniform way, where we don't necessarily care about the failure reason. They are generic over T.

There are two variants of Option<T>:

  • None, to indicate failure or lack of a value
  • Some(value), a wrapper around of value of type T

import { Option, Some, None } from 'iron-oxide';

// Array.prototype.find returns undefined if it doesn't find a value
// let's fix that
function find<T>(
    arr: T[],
    predicate: (value: T) => boolean
): Option<T> {
    const value = arr.find(v => predicate(v));

    // if the value is undefined, we return `None`, our failure state
    if (value === undefined) {
        return None();
    }

    // otherwise, we wrap the value in `Some`
    return Some(value);
}

Methods

Option comes with some built-in methods for interacting with the wrapped value and handling what happens when the Option is None.

Interacting with the value

From our introductory example, we now have a find function with the signature

function find<T>(arr: T[], predicate: (value: T) => boolean): Option<T>;

Commonly, we use the map method to transform the value.

// we have an array of people, and want to find Tom and increment
// his age
const people = [
    {
        name: "Bob",
        age: 25
    },
    {
        name: "Sally",
        age: 26
    },
    {
        name: "Tom",
        age: 28
    }
]; 

const newTom = find(people, ({ name }) => name === "Tom")
    .map(tom => ({
        ...tom,
        age: tom.age + 1
    });

Here, we find Tom and increment his age inside of map. But what happens if we look for someone else that doesn't exist?

const newLucy = find(people, ({ name }) => name === "Lucy")
    .map(lucy => ({
        ...lucy,
        age: lucy.age + 1
    });

Nothing will actually happen. We would expect that normally this would throw a nasty exception, something on the lines of lucy is not defined. In our case, find returns None , which won't cause the rest of our code to fail. But how do we figure out if the operation failed?

Checking for failure

Option comes with a few ways to check if the value is None, such as the isNone method

if (newLucy.isNone()) {
    console.log("Oops, we couldn't find lucy!");
}

Sometimes, we would rather the application throw if we find a None. We do this with the unwrap method

try {
    newLucy.unwrap();
catch (err) {
    // Throws "Called 'Option.unwrap' on a 'None' value"
    console.err(err);
}

isSome

Option.isSome returns whether or not the Option is a Some.

Option<T>.isSome = () => boolean;

Example

const carson = find(people, person => person.name === "Carson")

if (carson.isSome()) {
    console.log("We found Carson");
} else {
    console.log("We didn't find Carson");
}

isNone

Option.isNone returns whether or not the Option is None.

Option<T>.isNone = () => boolean;

Example

const carson = find(people, person => person.name === "Carson")

if (carson.isNone()) {
    console.log("We didn't find Carson");
} else {
    console.log("We found Carson");
}

unwrap

Option.unwrap returns the value contained in a Some and throws if the Option is None.

Option<T>.unwrap = () => T;

Example

const carson = find(people, person => person.name === "Carson");

try {
    carson.unwrap();
} catch (err) {
    console.error(err);
}

unwrapOr

Option.unwrapOr returns the value contained in a Some and returns the provided default if the Option is None.

Option<T>.unwrapOr = (def: T) => T;

Example

const carson = find(people, person => person.name === "Carson") // => None
        .unwrapOr({ name: "Carson", age: 27 });                 // => { name: "Carson", age: 27 }

unwrapOrElse

Option.unwrapOrElse returns the value contained in a Some and returns the result of calling the provided closure if the Option is None.

Option<T>.unwrapOrElse = (def: () => T) => T;

Example

const carson = find(people, person => person.name === "Carson") // => None
        .unwrapOrElse(() => { name: "Carson", age: 27 });       // => { name: "Carson", age: 27 }

map

Option.map transforms the contained value of the option using the provided projection.

Option<T>.map<U> = (proj: (value: T) => U) => Option<U>;

Example

find(people, person => person.name === 'Tom')   // => Some({ name: "Tom", age: 28 })
    .map(person => person.name)                 // => Some("Tom")

mapOr

Option.mapOr transforms the contained value and returns it, returning the default value if the Option is None.

Option<T>.mapOr<U> = (def: U, proj: (value: T) => U) => U;

Example

find(people, person => person.name === 'Carson') // => None
    .mapOr("Carson", person => person.name)        // => "Carson"

mapOrElse

Option.mapOr transforms the contained value and returns it, returning the value returned by the provided closure if the Option is None.

Option<T>.mapOrElse<U> = (def: () => U, proj: (value: T) => U) => U;

Example

const defaultName = "Carson";

find(people, person => person.name === 'Carson')    // => None
    .mapOrElse(() => defaultName, person => person.name)  // => "Carson"

okOr

Option.okOr transforms the Option into a Result. If the Option is None the provided error will be used as the error of Result.

Option<T>.okOr<E> = (err: E) => Result<T, E>;

Example

find(people, person => person.name === 'Carson')   // => None
    .okOr("Couldn't find Carson...")               // => Err("Couldn't find Carson...")

okOrElse

Option.okOrElse transforms the Option into a Result. If the Option is None the result of the provided error closure will be used as the error of Result.

Option<T>.okOr<E> = (err: () => E) => Result<T, E>

Example

const cantFindMessage = "Couldn't find Carson..."

find(people, person => person.name === 'Carson')  // => None
    .okOr(() => cantFindMessage)                  // => Err("Couldn't find Carson...")

and

Option.and returns None if the Option is None, otherwise returns optionB.

Option<T>.and<U> = (optionB: Option<U>) => Option<U>

Example

find(people, person => person.name === 'Tom')   // => Some({ name: "Tom", age: 28 })
    .and(Some({ profession: "Engineer" }))      // => Some({ profession: "Engineer" })

andThen

Option.andThen returns None if the Option is None, otherwise returns the option contained within the provided closure.

Option<T>.andThen<U> = (f: (value: T) => Option<U>) => Option<U>;

Example

find(people, person => person.name === 'Tom')     // => Some({ name: "Tom", age: 28 })
    .and(() => Some({ profession: "Engineer" }))  // => Some({ profession: "Engineer")

or

Option.or returns the option, or returns optionB if the Option is None.

Option<T>.or = (optionB: Option<T>) => Option<T>;

Example

const carson = find(people, person => person.name === 'Carson'); // => None

carson.or(find(people,person => person.name === 'Tom')) // => Some({ name: "Tom", age: 28 })

orElse

Option.orElse returns the option, or returns optionB returned by the given closure if the Option is None.

Option<T>.orElse = (f: () => Option<T>) => Option<T>;

Example

const carson = find(people, person => person.name === 'Carson'); // => None

carson.orElse(() => find(people,person => person.name === 'Tom')) // => Some({ name: "Tom", age: 28 })

filter

Option.filter returns the Some if the contained value satisfies the predicated, returning None otherwise.

Option<T>.filter = (predicate: (value: T) => boolean) => Option<T>;

Example

find(people, person => person.name === 'Tom')    // => Some({ name: "Tom", age: 28 })
    .filter(person => person.name === 'Carson')  // => None

zip

Option.zip combines two Options into a single Option of type Option<T, U>

Option<T>.zip<U> = (other: Option<U>) => Option<[T, U]>;

Example

const tom = find(people, person => person.name === 'Tom'); // => Some({ name: "Tom", age: 28 })
const bob = find(people, person => person.name === 'Bob'); // => Some({ name: "Bob", age: 25 })

tom.zip(bob) // => Some([{ name: "Tom", age: 28 }, { name: "Bob", age: 25 } ]);

expect

Option.expect returns the wrapped value, or throws an error with the provided message

Option<T>.expect = (message: string) => T;

Example

find(people, person => person.name === 'Carson') // => None
    .expect("Couldn't find Carson!")             // => throws!

Using Result

Sometimes we want to give more information about the failure of a function than just returning None. The Result type is used to do this. It is similar to Option in that it can wrap a value, but it can also wrap an error. Result is generic over T, the type of the value, and E, the type of the error.

There are two variants of Result<T, E>:

  • Ok(value), which indicates success and wraps the value of type T
  • Err(why), which indicates failure and wraps a why of type E, which should explain the cause of the failure
import { Result, Ok, Err } from 'iron-oxide';

// Number.parseInt returns NaN, which we would need to check for later
// let's fix that

function parseInt(
    maybeInt: string,
    radix?: number
): Result<number, string> {
    const result = parseInt(maybeInt, radix);

    if (isNaN(result)) {
        return Err(
            `Attempted to parse ${maybeInt} as an integer`
        );
    }

    return Ok(result);
}

Methods

Result comes with some built-in methods for interacting with the wrapped value and handling what happens when the Result is Err.

Interacting with the value

From our introductory example, we have our parseInt function with the signature

function parseInt(str: string, radix?: number): Result<number, string>;

Commonly, we use the map method to transform the value.


const numTimesTwo = parseInt("2")
    .map(n => n * 2)

Here, parseInt returns Ok(2), which can then be mapped as we see fit. In JavaScript, if this were to fail, it would result in NaN, or not a number, meaning we would need to work with isNaN in order to guard against the failure. Here, though, that's been taken care of for us inside of parseInt and it will return a nice error message, as an Err containing information about why the parsing failed. How do we then work with this error?

Interacting with the error

Result also comes with some methods for working with the error we receive. Most notably, errors can be mapped with mapErr.

const erroredParse = parseInt("foo")
    .mapErr(errorMessage => {
        return "Oops, we couldn't parse that value!"
    })

This can become very useful when we have much more complex return types


enum MathError {
    DivisionByZero,
    NonPositiveLogarithm,
    NegativeSquareRoot
}

type MathResult = Result<number, MathError>;

function div(x: number, y: number): MathResult {
    if (y === 0) {
        return Err(MathError.DivisionByZero);
    } else {
        return Ok(x / y);
    }
}

function ln(x: number): MathResult {
    if (x < 0) {
        return Err(MathError.NonPositiveLogarithm);
    } else {
        return Ok(Math.log10(x))
    }
}

function sqrt(x: number): MathResult {
    if (x < 0) {
        return Err(MathError.NegativeSquareRoot);
    } else {
        return Ok(Math.sqrt(x));
    }
}

In each of these instances, our error could be one of the enum MathError. This means we can tailor how we handle the error accordingly.


div(1, 0)
    .mapErr(err => {
        if (err === MathError.DivisionByZero) {
            throw new Error("You attempted to divide by zero!")
        }
    });

ln(-1)
    .mapErr(err => {
        if (err === MathError.NonPositiveLogarithm) {
            throw new Error("Natural log is not defined for values less than 0");
        }
    });

sqrt(-1)
    .mapErr(err => {
        if (err === MathError.NegativeSquareRoot) {
            throw new Error("The square root of a negative number is imaginary" )
        }
    })

map

Result.map transforms the contained value using the provided projection.

Result<T, E>.map = <U>(proj: (a: T) => U) => Result<U, E>;

Example


div(2, 2).map(n => n * 2); // Ok(2)

mapErr

Result.mapErr transforms the contained error using the provided projection.

Result<T, E>.mapErr = <F>(op: (err: E) => F) => Result<T, F>;

Example


div(2, 0).mapErr<string>(err => {
  if (err === MathError.DivisionByZero) {
    return 'You tried to divide by zero. That doesn\'t work'
  }

  return err;
}); // Err('You tried to divide by zero. That doesn\'t work')

mapOr

Result.mapOr transforms the contained value and returns it, returning the default value if the result is Err.

Result<T,E>.mapOr = <U>(def: U, proj: (a: T) => U) => U;

Example

div(2, 0).mapOr(0, n => n * 2) // Ok(0)

mapOrElse

Result.mapOr transforms the contained value and returns it, returning the value returned by the provided closure if the result is Err.

Result<T,E>.mapOrElse = <U>(def: () => U, proj: (a: T) => U) => U;

Example

div(2, 0).mapOr(() => 0, n => n * 2) // Ok(0)

isOk

Result.isOk returns whether or not the result is Ok

Result<T, E>.isOk(): boolean;

Example


div(2, 2).isOk(); // => true

div(2, 0).isOk(); // => false

isErr

Result.isErr returns whether or not the result is Err

Result<T, E>.isErr(): boolean;

Example


div(2, 2).isErr(); // => false

div(2, 0).isErr(); // => true

ok

Result.ok converts Result<T, never> into Option<T>, None if the result is Err.

Result<T, E>.ok = () => Option<T>;

Example


div(2, 2) // => Ok(1)
  .ok()   // => Some(1)

err

Result.err converts Result<never, E> into Option<E>, None if the result is Ok.

Result<T, E>.err = () => Option<E>;

Example


div(2, 2) // => Ok(1)
  .err()  // => None

and

Result.and returns the supplied res if the result is Ok, returns Err<never> otherwise.

Result<T, E>.and<U> = (res: Result<U, E>) => Result<U, E>;

Example


div(2, 2)         // => Ok(1)
  .and(div(3, 3)) // => Ok(1)

andThen

Result.andThen returns the result of the supplied res closure if the result is Ok, returns Err<never> otherwise.

Result<T, E>.andThen<U> = (op: (a: T) => Result<U, E>): Result<U, E>;

Example


div(2, 2)                   // => Ok(1)
  .andThen(() => div(3, 3)) // => Ok(1)

S

or

Result.or returns the option, or returns optionB if the result is Err.

Result<T, E>.or = (res: Result<T, E>) => Result<T, E>;

Example

div(2, 0)        // => Err(MathError.DivisionByZero)
  .or(div(1, 1)) // => Ok(1)

orElse

Result.orElse returns the option, or returns the result of the optionB closure if the result is Err.

Result<T, E>.orElse<F> = (op: (a: E) => Result<T, F>) => Result<T, F>;

Example

div(2, 0)                  // => Err(MathError.DivisionByZero)
  .orElse(() => div(1, 1)) // => Ok(1)

unwrap

Result.unwrap returns the value contained in an Ok and throws if the result is Err.

Result<T,E>.unwrap = () => T;

Example

  div(1, 1).unwrap(); // => 1

unwrapErr

Result.unwrapErr returns the error contained in an Err and throws if the result is Ok.

Result<T,E>.unwrapErr = () => E;

Example

  div(1, 0).unwrap(); // => MathError.DivisionByZero

unwrapOr

Result.unwrapOr returns the value contained in an Ok and returns the provided default if the result is Err.

Result<T, E>.unwrapOr = (optb: T) => T;

Example

div(1, 0).unwrapOr(0); // => 0

unwrapOr

Result.unwrapOr returns the value contained in an Ok and returns the result of calling the provided closure if the result is Err.

Result<T, E>.unwrapOrElse = (op: (err: E) => T) => T;

Example

div(1, 0).unwrapOrElse(() => 0); // => 0

expect

Result.expect returns the contained value, throwing the provided message if the result is Err.

Result<T, E>.expect = (msg: string) => T;

Example


div(2, 0)                                         // => Ok(1)
  .expect("Something went wrong while dividing")  // => None

expectErr

Result.expectErr returns the contained error, throwing the provided message if the result is Ok.

Result<T, E>.expectErr = (msg: string) => E;

Example


div(2, 0)                                         // => Ok(1)
  .expectErr("Something went wrong while dividing")  // => Some("Somethign went wrong while dividing")

Using match

iron-oxide offers a rudimentary way to mimic rust's match keyword. In rust the match keyword is used similarly to the way a switch statement is used in JavaScript and TypeScript, except for one key difference: match works with more than just primitive values.

API

match takes two arguments

  • The value, of type T, we are matching
  • A list of MatchStatements, which are generic over T the type of the value, and R the return value of the match

Each match statement is a tuple of two things

  • The value, of type T, or a predicate function which takes the value and returns a boolean
  • The function to call if the predicate matches. This receives the matched value

If no statement is matched, and matchUtils.fallback is not given, match will throw an error.

Example

With a primitive value

import { match } from 'iron-oxide';

function getParity(n: number) {
    return match(n, [
        [0, () => 'neitherEvenNorOdd'],
        [x => x % 2 === 0, () => 'even'],
        [x => x % 2 !== 0. () => 'odd']
    ]);
}

With an Option (here, find returns Option<Person>)

import { match } from 'iron-oxide`;

function getPerson(name: string) {
    const maybePerson = find(people, person => person.name === name);

    return match(maybePerson, [
        [isNone, () => console.log("Couldn't find a person with the name:", name)],
        [isSome, (person) => console.log("Found the person:", person)]
    ]);
}

matchUtils

iron-oxide also exports a set of utilities that can be used with a matchStatement, these are

  • isOption, matches any Option
  • isSome, matches any Some
  • isNone, matches any None
  • isResult, matches any Result
  • isOk, matches any Ok
  • isErr, matches any Err
  • fallthrough, which matches any and all values