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:
Somerepresenting success or a valueNonerepresenting failure or lack of a value
Result also comes in two flavors:
Okrepresenting success or a valueErrrepresenting 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-oxideis 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 valueSome(value), a wrapper around ofvalueof typeT
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 thevalueof typeTErr(why), which indicates failure and wraps awhyof typeE, 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 overTthe type of the value, andRthe return value of thematch
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 anyOptionisSome, matches anySomeisNone, matches anyNoneisResult, matches anyResultisOk, matches anyOkisErr, matches anyErrfallthrough, which matches any and all values