Type-safe implementation of lightweight Option and Result types in TypeScript (and JavaScript).
import { Option, Some, None } from "rstypes/option";
function index_in_array<T>(array: T[], search: T): Option<number> {
let index = array.indexOf(search);
if(index == -1) {
return None;
} else {
return Some(index);
}
}
let idx = index_in_array([1, 2, 3], 2);
idx.match({
Some(value) { console.log("Found at index: ", value) },
None() { console.log("Could not find in array") }
})import { Result, Ok, Err } from "rstypes/result"; // ESM
const { Result, Ok, Err } = require("rstypes/result"); // CommonJS
function parse_int(str: string): Result<number, string> {
let maybe_number = parseInt(str);
if(isNaN(maybe_number)) return Err("Invalid string");
return Ok(maybe_number);
}
let result = parse_int("333");
result.match({
Ok(num) { console.log("Parsed number: ", num) },
Err(msg) { console.log("Error parsing: ", msg) }
});You can install rstypes on npm:
npm install rstypesUse Option<T> to represent an optional value, for example:
- An explicit optional parameter in a function.
- An object that may or may not have a value in its field, but should always have that field.
- A function that may or may not return a value.
Use Result<T, E> to represent either a successful return value or an error that is expected and recoverable, for example:
- A parsing function that can fail with malformed input.
- A function that can error based on external state.
- A function that can fail in multiple ways that should be handled separately.
These types give you a type-safe alternative to returning null or undefined and throwing exceptions, in ways that can be handled more explicitly and are immediately clear from the function signature.
Option and Result values can be handled almost identically (The examples below will focus on Result but will clarify differences with handling Options).
Consider an example Result variable:
let res: Result<number, string> = /* ... */Where number is the Ok type and string is the Err type.
Matching on values is exhaustive, and can be done using a more verbose Rust-like syntax using an object, or simply two arrow functions for conciseness. This is considered the default way to handle values for its expressiveness and flexibility.
res.match({
Ok(value) { /* do something with value */ },
Err(error) { console.error(error); }
})
let x = res.match(
value => 2 * value,
error => { console.error("There was an error"); return 0; }
);N.B.
Optionvalues match on the two functionsSome(value)andNone()instead, where theNonecase takes no arguments.
These two methods should only be used when you are sure that the value cannot be Err/None and just want immediate access to the inner value (ie. when the error case would violate a fundamental assumption of the program). These functions will hard error if called on Err/None, with unwrap throwing a generic error and expect throwing an error with the specified message:
let x: number = res.unwrap();
let y: number = res.expect("Should never error with the given inputs");These two methods should be used when you don't care to handle the error case and want suitable default behaviour instead.
let x: number = res.unwrap_or(0);
let y: number = res.unwrap_or_else(() => Math.random()); // the function can also depend on the error value!These two methods return true or false when the Result value is the appropriate variant. They can be used for more traditional/non-exhaustive handling, and TypeScript will automatically narrow the type to the respective variant within their blocks.
if(res.is_ok()) {
let x = res.unwrap(); // Guaranteed not to fail
// ...
} else {
console.log("There was some error");
}N.B.
Optionvalues have analogous predicatesis_someandis_none.
These two methods return true when the result value is of the appropriate variant and a given predicate evaluates to true with the inner value, returning false otherwise. They can be used to check for a given property of contained values.
if(res.is_ok_and(n => n % 2 == 0)) {
let even = res.unwrap();
//...
}N.B.
Optionvalues have equivalentis_some_andandis_none_or, with the latter returning true if the option isNone, orSome(value)with the predicate being true forvalue.
These two methods can convert between Result values with a different Ok type and Err type respectively, propagating the alternate value otherwise. They can be used to perform transformations conditionally.
let s: Result<string, string> = res.map((n) => `number ${n}`);
let e: Result<number, Error> = res.map_err((e) => Error(e));N.B.
Optionvalues only havemapto translate betweenOption<T>andOption<U>.
These two methods convert a Result<T,E> into an Option<T> and Option<E> respectively, returning Some(value) if the variant matches the method called and None otherwise. They can be used to translate between the two types as needed.
let n: Option<number> = res.ok();
let s: Option<number> = res.err();N.B.
Optionvalues have an equivalentok_or(error)method to translateSome(value)intoOk(value)andNoneintoErr(error).
These two methods can be used to perform logic on Result values, evaluating to the alternative given if the first result is Ok or Err respectively, or itself otherwise.
let res2: Result<number, string> = Err("something");
let or = res2.or(res) // or == res
let and = res2.and(res); // or == Err("something")N.B.
Optionvalues also have anxormethod that performs similar logic, evaluating toNonewhen both options areSomeorNoneand the onlySome(value)otherwise.
This library also offers two wrappers to convert other JavaScript functions into these patterns:
as_resulttakes a function that can throw and outputs a function that returns aResult, withOkif it returned andErrif it threw.
import { as_result } from "rstypes/result";
let parse_json = as_result(JSON.parse);
let parsed: Result<any, SyntaxError> = parse_json("{}"); // parsed = {}
let error: Result<any, SyntaxError> = parse_json("abcd"); // error = Err(SyntaxError(...))as_optiontakes a function that can returnNaN,nullorundefinedand outputs a function that returns anOption, withNoneif it returned one of those values orSomeotherwise.
import { as_option } from "rstypes/option";
let sqrt = as_option(Math.sqrt);
let y1: Option<number> = sqrt(1); // y1 = Some(1)
let y2: Option<number> = sqrt(-1); // y2 = NoneFor additional type safety (such as not being able to call unwrap on directly instantiated Err values), the outputs of Ok(x: T) and Err(e: E) are not considered to be values of Result<T, E> but rather of their own individual types: Ok<T, unknown> and Err<unknown, E>. This is also due to the impossibility of inferring a T type from a construction of Err<unknown, E> and vice versa, resulting in confusing type signatures when returning both from functions.
Therefore, for best developer experience, take care to use explicit Result<T, E> type annotations where possible.
N.B. Holds analogously for
SomeandNone.