Skip to content

Rust's Option<T> and Result<T, E>, implemented for TypeScript.

License

Notifications You must be signed in to change notification settings

scalarhq/oxide

 
 

Repository files navigation

oxide.ts

Rust's Option<T> and Result<T, E>, implemented for TypeScript.

Features

Zero dependencies, full test coverage and examples for every function at your fingertips with JSDoc comments.

  • Add more meaning to return types.
  • Express, chain and map values as if you were writing in Rust.
  • Use the match adaptation to simplify conditionals.
  • Quickly test multiple Results or Options with .all and .any.
  • Convert throws and rejections into Results and Options with .safe.

Patch 0.9.10

  • Make guarded functions that return at the first sign of trouble (?).
  • API available both snake_case and camelCase.

Installation

$ npm install oxide.ts --save

Usage

The the best documentation is in the JSDoc and tests directory, there are several examples there not covered in this Readme. If you're using VSCode you should also be able to hover over methods to see some examples.

Core Features

Advanced Features

Tests

npm run test

Option

An Option represents either something, or nothing. If we hold a value of type Option<T>, we know it is either Some<T> or None. Both types share a common API, so we can chain operations without having to worry whether we have Some or None until pulling the value out:

import { Option, Some, None } from "oxide.ts";

function divide(x: number, by: number): Option<number> {
   if (by === 0) {
      return None;
   } else {
      return Some(x / by);
   }
}

const val = divide(100, 20);

// Pull the value out, or throw if None:
const res: number = val.unwrap();
// Throw our own error message in the case of None:
const res: number = val.expect("Division Failed");
// Pull the value out, or use a default if None:
const res: number = val.unwrapOr(1);

// Map the Option<T> to Option<U> by applying a function:
const strval: Option<string> = val.map((num) => `Result = ${num}`);
// Then unwrap the value or use a default if None:
const res: string = strval.unwrapOr("Error");
// Map, assign a default and unwrap in one line:
const res: string = val.mapOr("Error", (num) => `Result = ${num}`);

The type annotations applied to the const variables are for information - the correct types would be inferred.

« To contents

Result

A Result represents either something good (T) or something not so good (E). If we hold a value of type Result<T, E> we know it's either Ok<T> or Err<E>. You could think of a Result as an Option where None has a value.

import { Result, Ok, Err } from "oxide.ts";

function divide(x: number, by: number): Result<number, string> {
   if (by === 0) {
      return Err("Division Failed");
   } else {
      return Ok(x / by);
   }
}

const val = divide(100, 20);

// These are the same as Option (as are many of the other methods):
const res: number = val.unwrap();
const res: number = val.expect("Division Failed");
const res: number = val.unwrapOr(1);
// Map Result<T, E> to Result<U, E>, similar to mapping Option<T> to Option<U>
const strval: Result<string, string> = val.map((num) => `Result = ${num}`);
const res: string = strval.unwrapOr("Error");
const res: string = val.mapOr("Error", (num) => `Result = ${num}`);

// We can unwrap the error, which throws if the Result is Ok:
const err: string = val.unwrapErr();
const err: string = val.expectErr("Expected this to fail");

// Or map the error, mapping Result<T, E> to Result<T, F>
const objerr: Result<number, Error> = val.mapErr((message) => {
   return new Error(message);
});

« To contents

Transformation

Because they are so similar, it's possible to transform an Option<T> into a Result<T, E> and vice versa:

const val: Option<number> = divide(100, 10);

// Here, the argument provides the Err value to be used if val is None:
const res: Result<number, string> = val.okOr("Division Error");

// And to turn it back into an Option:
const opt: Option<number> = res.ok();

Note that converting from Result<T, E> to Option<T> causes the Err value (if any) to be discarded.

« To contents

Nesting

There is no reason you can't nest Option and Result structures. The following is completely valid:

const res: Result<Option<number>, string> = Ok(Some(10));
const val: number = res.unwrap().unwrap();

There are times when this makes sense, consider something like:

import { Result, Option, Some, None, Ok, Err, match } from "oxide.ts";

function search(query: string): Result<Option<SearchResult>, string> {
   const [err, result] = database.search(query);
   if (err) {
      return Err(err);
   } else {
      return result.count > 0 ? Ok(Some(result)) : Ok(None);
   }
}

const result = search("testing");
const output: string = match(result, {
   Ok: match({
      Some: (res) => `Found ${res.count} entries.`,
      None: () => "No results for that search.",
   }),
   Err: (err) => `Error: ${err}`,
});

« To contents

Match

Concisely determine what action should be taken for a given input value. For all the different ways you can use match (including the advanced uses discussed later), the following rules apply:

  • Every branch must have the same return type.
  • As soon as a matching branch is found, no others are checked.

The most basic match can be performed on Option and Result types. This is called mapped matching.

const num: Option<number> = Some(10);
const res = match(num, {
   Some: (n) => n + 1,
   None: () => 0,
});

assert.equal(res, 11);

It's also possible to nest mapped matching and provide defaults. You don't have to include every named branch:

const matchNest = (input: Result<Option<number>, string>) =>
   match(input, {
      Ok: match({
         Some: (n) => `num ${n}`,
      }),
      _: () => "nothing",
   });

assert.equal(matchNest(Ok(Some(10))), "num 10");
assert.equal(matchNest(Ok(None)), "nothing");
assert.equal(matchNest(Err("none")), "nothing");

Note: Using match without the first-position value is not a way to "compile" a match function. Only call match like this within a nested match structure.

« To contents

Safe

Capture the outcome of a function or Promise as an Option<T> or Result<T, E>, preventing throwing (function) or rejection (Promise).

Safe Functions

Calls the passed function with the arguments provided and returns an Option<T> or Result<T, Error>. The outcome is Some/Ok if the function returned, or None/Err if it threw. In the case of Result.safe, any thrown value which is not an Error is converted.

function mightThrow(throws: boolean) {
   if (throws) {
      throw new Error("Throw");
   }
   return "Hello World";
}

const x: Result<string, Error> = Result.safe(mightThrow, true);
assert.equal(x.unwrapErr() instanceof Error, true);
assert.equal(x.unwrapErr().message, "Throw");

const x = Result.safe(() => mightThrow(false));
assert.equal(x.unwrap(), "Hello World");

Note: Any function which returns a Promise (or PromiseLike) value is rejected by the type signature. Result<Promise<T>, Error> or Option<Promise<T>> are not useful types - using it in this way is likely to be a mistake.

Safe Promises

Accepts a Promise and returns a new Promise which always resolves to either an Option<T> or Result<T, Error>. The Result is Some/Ok if the original promise resolved, or None/Err if it rejected. In the case of Result.safe, any rejection value which is not an Error is converted.

async function mightThrow(throws: boolean) {
   if (throws) {
      throw new Error("Throw");
   }
   return "Hello World";
}

const x = await Result.safe(mightThrow(true));
assert.equal(x.unwrapErr() instanceof Error, true);
assert.equal(x.unwrapErr().message, "Throw");

const x = await Result.safe(mightThrow(false));
assert.equal(x.unwrap(), "Hello World");

« To contents

All

Reduce multiple Options or Results to a single one. The first None or Err encountered is returned, otherwise the outcome is a Some/Ok containing an array of all the unwrapped values.

function num(val: number): Result<number, string> {
   return val > 10 ? Ok(val) : Err(`Value ${val} is too low.`);
}

const xyz = Result.all(num(20), num(30), num(40));
const [x, y, z] = xyz.unwrap();
assert.equal(x, 20);
assert.equal(y, 30);
assert.equal(z, 40);

const err = Result.all(num(20), num(5), num(40));
assert.equal(err.isErr(), true);
assert.equal(err.unwrapErr(), "Value 5 is too low.");

« To contents

Any

Reduce multiple Options or Results into a single one. The first Some/Ok found (if any) is returned, otherwise the outcome is None, or in the case of Result - an Err containing an array of all the unwrapped errors.

function num(val: number): Result<number, string> {
   return val > 10 ? Ok(val) : Err(`Value ${val} is too low.`);
}

const x = Result.any(num(5), num(20), num(2));
assert.equal(x.unwrap(), 20);

const efg = Result.any(num(2), num(5), num(8));
const [e, f, g] = efg.unwrapErr();
assert.equal(e, "Value 2 is too low.");
assert.equal(f, "Value 5 is too low.");
assert.equal(g, "Value 8 is too low.");

« To contents

Advanced Features

Word to the wise

The match adaptation shifts the TypeScript idiom and may not be suitable for your project - especially if you work with others.

Combined Matching

It's possible to combine the mapped and chained matching approach.

import { Option, match } from "oxide.ts";

// Easiest to build upon
function player_allowed(player: Option<Player>): boolean {
   return match(player, {
      Some: [
         [{ status: "banned" }, false],
         [{ age: (n) => n > 18 }, true],
      ],
      _: () => false,
   });
}

« To contents

Chained Matching

Can be performed on any type. A chain is an array of branches which are tested in sequence. A branch is a tuple of [<condition>, <result>]. Chain branches follow the following rules:

  • Primitive comparisons test for exact equality (===).
  • Any comparison with the condition _ (Default) succeeds automatically.
  • Matching against arrays is a key-to-key comparison (just like objects). As such, a match condition of [10, 20] doesn't check if 10 and 20 are in the array, but instead checks specifically that index 0 is 10 and index 1 is 20.
  • Tuple elements are "functions first", such that any <condition> that is a function will be called to determine if the branch matches, and any <result> that is a function is called with the input value to determine the return value. To match or return a function, see Fn.
  • On the matter of functions, a <condition> is always a sync function. A <result> can be async, but if so every branch must return an async function.
  • Option and Result types are recursively evaluated to their deepest reachable values and evaluated like any other condition. Using mapped or combined matching for these types is better.

At the end of a chain, an optional default branch may be included which is called with the input value when no other branch matches. If no default is provided, match will throw an error if no other branch matches.

Note: Deeply nesting Option/Result matches may not allow for complete type information to be presented to the user (though they should still be verified). It is also slower (execution time and type computation) than mapped matching or combined matching.

Primitive Example

import { match } from "oxide.ts";

const matchNum = (num: number) =>
   match(num, [
      [5, "five"],
      [(n) => n > 100, "big number"],
      [(n) => n < 0, (n) => `negative ${n}`],
      () => "other",
   ]);

assert.equal(matchNum(5), "five");
assert.equal(matchNum(150), "big number");
assert.equal(matchNum(-20), "negative -20");
assert.equal(matchNum(50), "other");

Object Example

import { match } from "oxide.ts";

const matchObj = (obj: { a: number; b: { c: number } }) =>
   match(obj, [
      [{ a: 5 }, "a is 5"],
      [{ b: { c: 5 } }, "c is 5"],
      [{ a: 10, b: { c: (n) => n > 10 } }, "a 10 c gt10"],
      () => "other",
   ]);

assert.equal(matchObj({ a: 5, b: { c: 5 } }), "a is 5");
assert.equal(matchObj({ a: 50, b: { c: 5 } }), "c is 5");
assert.equal(matchObj({ a: 10, b: { c: 20 } }), "a 10 c gt 10");
assert.equal(matchObj({ a: 8, b: { c: 8 } }), "other");

Array Example

import { match, _ } from "oxide.ts";

const matchArr = (arr: number[]) =>
   match(arr, [
      [[1], "1"],
      [[2, (n) => n > 10], "2 gt10"],
      [[_, 6, _, 12], "_ 6 _ 12"],
      () => "other",
   ]);

assert.equal(matchArr([1, 2, 3]), "1");
assert.equal(matchArr([2, 12, 6]), "2 gt10");
assert.equal(matchArr([3, 6, 9, 12]), "_ 6 _ 12");
assert.equal(matchArr([2, 4, 6]), "other");

« To contents

About

Rust's Option<T> and Result<T, E>, implemented for TypeScript.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 99.9%
  • JavaScript 0.1%