oxide.ts
Rust's Option<T>
and Result<T, E>
, implemented
for TypeScript. Zero dependencies, full test coverage and complete in-editor documentation.
Installation
$ npm install oxide.ts --save
New features in 1.1.0
- Added intoTuple to
Result
- Added
flatten
to Option
and Result
Usage
Core Features
Advanced
Importing
You can import the complete oxide.ts library:
import { Option, Some, None, Result, Ok, Err, match, Fn, _ } from "oxide.ts";
Or just the core library, which exclues the match
feature:
import { Option, Some, None, Result, Ok, Err } from "oxide.ts/core";
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> {
return by === 0 ? None : Some(x / by);
}
const val = divide(100, 20);
const res: number = val.unwrap();
const res: number = val.expect("Don't divide by zero!");
const res: number = val.unwrapOr(1);
const strval: Option<string> = val.map((num) => `val = ${num}`);
const res: string = strval.unwrapOr("val = <none>");
const res: string = val.mapOr("val = <none>", (num) => `val = ${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> {
return by === 0 ? Err("Division by zero") : Ok(x / by);
}
const val = divide(100, 20);
const res: number = val.unwrap();
const res: number = val.expect("Don't divide by zero!");
const res: number = val.unwrapOr(1);
const strval: Result<string, string> = val.map((num) => `val = ${num}`);
const res: string = strval.unwrapOr("val = <err>");
const res: string = val.mapOr("val = <err>", (num) => `val = ${num}`);
const err: string = val.unwrapErr();
const err: string = val.expectErr("Expected division by zero!");
const errobj: Result<string, Error> = val.mapErr((msg) => new Error(msg));
Converting
These methods provide a way to jump in to (and out of) Option
and Result
types. Particularly these methods can streamline things where:
- A function returns
T | null
, T | false
or similar. - You are working with physical quantities or using an
indexOf
method. - A function accepts an optional argument,
T | null
or similar.
Note: Converting to a Result often leaves you with a Result<T, null>
.
The null value here is not very useful - consider the equivalent Option method
to create an Option<T>
, or use mapErr
to change the E
type.
into
Convert an existing Option
/Result
into a union type containing T
and
undefined
(or a provided falsey value).
function maybeName(): Option<string>;
function maybeNumbers(): Result<number[], Error>;
function printOut(msg?: string): void;
const name: string | undefined = maybeName().into();
const name: string | null = maybeName().into(null);
const numbers: number[] | undefined = maybeNumbers().into();
const numbers: number[] | false = maybeNumbers().into(false);
printOut(name.into());
intoTuple
Convert a Result<T, E>
into a tuple of [null, T]
if the result is Ok
,
or [E, null]
otherwise.
function getUsername(): Result<string, Error>;
const query = getUsername();
const [err, res] = query.intoTuple();
if (err) {
console.error(`Query Error: ${err}`);
} else {
console.log(`Welcome: ${res.toLowerCase()}`);
}
from
Convert to an Option
/Result
which is Some<T>
/Ok<T>
unless the value is
falsey, an instance of Error
or an invalid Date
.
The T
is narrowed to exclude any falsey values or Errors.
const people = ["Fry", "Leela", "Bender"];
const person = Option.from(people.find((name) => name === "Fry"));
const person = Option(people.find((name) => name === "Bender"));
In the case of Result
, the E
type includes:
null
(if val
could have been falsey or an invalid date)Error
types excluded from T
(if there are any)
function randomName(): string | false;
function tryName(): string | Error;
function randomNumbers(): number[] | Error;
const person = Result.from(randomName());
const name = Result(tryName());
const num = Result(randomNumbers());
nonNull
Convert to an Option
/Result
which is Some<T>
/Ok<T>
unless the value
provided is undefined
, null
or NaN
.
function getNum(): number | null;
const num = Option.nonNull(getNum()).unwrapOr(100);
const words = ["express", "", "planet"];
const str = Option.nonNull(words[getNum()]);
str.unwrapOr("No such index");
qty
Convert to an Option
/Result
which is Some<number>
/Ok<number>
when the provided val
is a finite integer greater than or equal to 0.
const word = "Buggalo";
const g = Option.qty(word.indexOf("g"));
assert.equal(g.unwrap(), 2);
const z = Option.qty(word.indexOf("z"));
assert.equal(z.isNone(), true);
« To contents
Nesting
You can nest Option
and Result
structures. The following example uses
nesting to distinguish between found something, found nothing and
database error:
function search(query: string): Result<Option<SearchResult>, string> {
const [err, result] = database.search(query);
if (err) {
return Err(err);
} else {
return Ok(result.count > 0 ? Some(result) : None);
}
}
const result = search("testing");
const output: string = match(result, {
Ok: {
Some: (result) => `Found ${result.count} entries.`,
None: () => "No results for that search.",
},
Err: (err) => `Error: ${err}.`,
});
« To contents
Iteration
An Option
or Result
that contains an iterable T
type can be iterated upon
directly. In the case of None
or Err
, an empty iterator is returned.
The compiler will complain if the inner type is not definitely iterable
(including any
), or if the monad is known to be None
or Err
.
const numbers = Option([1.12, 2.23, 3.34]);
for (const num of numbers) {
console.log("Number is:", num.toFixed(1));
}
const numbers: Option<number[]> = None;
for (const num of numbers) {
console.log("Unreachable:", num.toFixed());
}
It's also possible to iterate over nested monads in the same way:
const numbers = Option(Result(Option([1, 2, 3])));
for (const num of numbers) {
console.log("Number is:", num.toFixed(1));
}
« 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 Option
s or Result
s 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 Option
s or Result
s 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
Match
Mapped matching is possible on Option
and Result
types:
const num = Option(10);
const res = match(num, {
Some: (n) => n + 1,
None: () => 0,
});
assert.equal(res, 11);
You can nest mapped matching patterns and provide defaults. If a default is
not found in the current level it will fall back to the previous level. When
no suitable match or default is found, an exhausted error is thrown.
function nested(val: Result<Option<number>, string>): string {
return match(val, {
Ok: { Some: (num) => `found ${num}` },
_: () => "nothing",
});
}
assert.equal(nested(Ok(Some(10))), "found 10");
assert.equal(nested(Ok(None)), "nothing");
assert.equal(nested(Err("Not a number")), "nothing");
« To contents
Combined Match
Mapped Matching and Chained Matching can be
combined. A match chain can be provided instead of a function for Some
,
Ok
and Err
.
function matchNum(val: Option<number>): string {
return match(val, {
Some: [
[5, "5"],
[(x) => x < 10, "< 10"],
[(x) => x > 20, "> 20"],
],
_: () => "none or not matched",
});
}
assert.equal(matchNum(Some(5)), "5");
assert.equal(matchNum(Some(7)), "< 10");
assert.equal(matchNum(Some(25)), "> 20");
assert.equal(matchNum(Some(15)), "none or not matched");
assert.equal(matchNum(None), "none or not matched");
« To contents
Match Chains
Chained matching is possible on any type. Branches are formed by associating
a condition
with a result
(with an optional default at the end). The first
matching branch is the result.
More detail about chained matching patterns is available in the bundled JSDoc.
Examples
function matchArr(arr: number[]): string {
return match(arr, [
[[1], "1"],
[[2, (x) => x > 10], "2, > 10"],
[[_, 6, 9, _], (a) => a.join(", ")],
() => "other",
]);
}
assert.equal(matchArr([1, 2, 3]), "1");
assert.equal(matchArr([2, 12, 6]), "2, > 10");
assert.equal(matchArr([3, 6, 9]), "other");
assert.equal(matchArr([3, 6, 9, 12]), "3, 6, 9, 12");
assert.equal(matchArr([2, 4, 6]), "other");
interface ExampleObj {
a: number;
b?: { c: number };
o?: number;
}
function matchObj(obj: ExampleObj): string {
return match(obj, [
[{ a: 5 }, "a = 5"],
[{ b: { c: 5 } }, "c = 5"],
[{ a: 10, o: _ }, "a = 10, o = _"],
[{ a: 15, b: { c: (n) => n > 10 } }, "a = 15; c > 10"],
() => "other",
]);
}
assert.equal(matchObj({ a: 5 }), "a = 5");
assert.equal(matchObj({ a: 50, b: { c: 5 } }), "c = 5");
assert.equal(matchObj({ a: 10 }), "other");
assert.equal(matchObj({ a: 10, o: 1 }), "a = 10, o = _");
assert.equal(matchObj({ a: 15, b: { c: 20 } }), "a = 15; c > 10");
assert.equal(matchObj({ a: 8, b: { c: 8 }, o: 1 }), "other");
« To contents
Compiling
Match patterns can also be compiled into a function. More detail about
compiling is available in the bundled JSDoc.
const matchSome = match.compile({
Some: (n: number) => `some ${n}`,
None: () => "none",
});
assert.equal(matchSome(Some(1)), "some 1");
assert.equal(matchSome(None), "none");
« To contents
« To top of page