A TypeScript monad library inspired by Rust, providing Result and Option types for safe error handling and null management with functional programming patterns.
JavaScript/TypeScript error handling often relies on try...catch blocks or nullable return types, which can be verbose or hide potential errors. rustify brings Rust-inspired monads like Result and Option to TypeScript, enabling functional programming patterns for safer code. This allows you to:
- Handle errors explicitly: Functions return a
Resultwhich is eitherOk(value)for success orErr(error)for failure. - Manage nullable values safely: Use
Optionto represent values that may or may not exist, eliminating null/undefined errors. - Improve type safety: Both
Result<T, E>andOption<T>types are tracked by the type system. - Chain operations safely: Monadic methods like
andThen,map, andorElseallow elegant functional composition. - Perform exhaustive checks: The
matchmethod ensures you handle all cases explicitly. - Easily wrap unsafe functions:
Result.fromandOption.fromNullableprovide simple ways to convert potentially unsafe operations. - Destructure results easily: Use
asTuple()for Go-style[err, val]destructuring, orasObject()if you prefer{ error, value }destructuring.
You can install rustify using your favorite package manager or directly from jsr.
npm:
npm install @ghaerdi/rustify
# or
yarn add @ghaerdi/rustify
# or
pnpm add @ghaerdi/rustifyjsr:
npx jsr add @ghaerdi/rustify
# or
bunx jsr add @ghaerdi/rustify
# or
deno add @ghaerdi/rustifyImport Ok, Err, Result, Some, None, and Option from the library.
import { Result, Ok, Err, Option, Some, None } from "@ghaerdi/rustify";
// --- Creating a function that returns a Result ---
// Example: A function that performs division but returns Err for division by zero
function divide(numerator: number, denominator: number): Result<number, string> {
if (denominator === 0) {
return Err("Cannot divide by zero"); // Failure case
}
const result = numerator / denominator;
return Ok(result); // Success case
}
// --- Using the function and handling the Result ---
const result = divide(10, 2); // Try change 2 to 0 for an Err case
// Use 'match' to exhaustively handle both Ok and Err cases.
// This is often the clearest way to ensure all possibilities are handled.
const messageFromResult = result.match({
Ok: (value) => {
// This runs only if result is Ok
console.log(`Match Ok (Result): Division successful, value is ${value}`);
return `Result: ${value}`;
},
Err: (errorMessage) => {
// This runs only if result is Err
console.error("Match Err (Result):", errorMessage);
return `Error: ${errorMessage}`;
}
});
console.log(messageFromResult);
// Example with Option
const anOptionalString: Option<string> = Some("Hello, Option!");
const messageFromOption = anOptionalString.match({
Some: (value) => `Option has value: ${value}`,
None: () => "Option is None"
});
console.log(messageFromOption);
// Working with ok() and err() methods that now return Option:
const okValue = result.ok(); // Returns Option<number>
if (okValue.isSome()) {
console.log(`Ok value: ${okValue.unwrap()}`);
}
const errValue = result.err(); // Returns Option<string>
if (errValue.isSome()) {
console.log(`Error: ${errValue.unwrap()}`);
}
// Other methods like Result.from, isOk, map, andThen, unwrapOrElse, asTuple etc.
// and Option.fromNullable, isSome, map, unwrapOr etc.
// allow for wrapping functions, specific checks, transformations, and handling patterns.
// See the API Overview sections for more details.Result<T, E>: Represents either success (Ok<T>) or failure (Err<E>).Ok<T>: Contains a success value. Becomes iterable ifTis iterable.Err<E>: Contains an error value.
Option<T>: Represents an optional value, eitherSome<T>orNone.Some<T>: Contains a value. Becomes iterable ifTis iterable.None(): Represents the absence of a value. CallNone()to create a None instance. You can check if an option is None using the.isNone()method.
The Result type provides numerous methods for handling and transformation:
- Checking:
isOk(): ReturnstrueifOk.isErr(): ReturnstrueifErr.isOkAnd(fn): ReturnstrueifOkand the value satisfiesfn.isErrAnd(fn): ReturnstrueifErrand the error satisfiesfn.
- Extracting Values:
ok(): Returns theOkvalue asSome(value)orNone.err(): Returns theErrvalue asSome(error)orNone.unwrap(): Returns theOkvalue, throws ifErr. Use with caution.unwrapErr(): Returns theErrvalue, throws ifOk.expect(message): ReturnsOkvalue, throwsmessageifErr.expectErr(message): ReturnsErrvalue, throwsmessageifOk.unwrapOr(defaultValue): ReturnsOkvalue ordefaultValueifErr.unwrapOrElse(fn): ReturnsOkvalue or computes default usingfn(errorValue)ifErr.
- Mapping & Transformation:
map(fn): MapsOk<T>toOk<U>. LeavesErruntouched.mapErr(fn): MapsErr<E>toErr<F>. LeavesOkuntouched.mapOr(defaultValue, fn): AppliesfntoOkvalue, returnsdefaultValueifErr.mapOrElse(defaultFn, fn): AppliesfntoOkvalue, appliesdefaultFntoErrvalue.
- Chaining & Side Effects:
and(res): ReturnsresifOk, else returns self (Err).andThen(fn): Callsfn(okValue)ifOk, returns the resultingResult.or(res): ReturnsresifErr, else returns self (Ok).orElse(fn): Callsfn(errValue)ifErr, returns the resultingResult.inspect(fn): Callsfn(okValue)ifOk, returns originalResult.inspectErr(fn): Callsfn(errValue)ifErr, returns originalResult.
- Pattern Matching:
match(matcher): Executesmatcher.Ok(value)ormatcher.Err(error), returning the result.
- Cloning:
cloned(): Returns a newResultwith a deep clone of theOkvalue (usingstructuredClone).Errvalues are not cloned.
- Destructuring / Representation:
asTuple(): Represents the Result's state as a tuple[error, value]. Returns[undefined, T]forOk(T)and[E, undefined]forErr(E).asObject(): Represents the Result's state as an object{ error, value }. Returns{ error: undefined, value: T }forOk(T)and{ error: E, value: undefined }forErr(E).
- Utilities (Static Methods on
Result):Result.from(fn, errorTransform?): Wraps a sync functionfnthat might throw. Executesfn. ReturnsOk(result)orErr(error).Result.fromAsync(fn, errorTransform?): Wraps an async functionfnreturning a Promise. ReturnsPromise<Result>. Handles resolution/rejection.Result.isResult(value): Type guard, returnstrueifvalueisOkorErr.
The Option<T> type represents an optional value: every Option is either Some and contains a value, or None and does not. It's useful for handling cases where a value might be absent, without resorting to null or undefined directly, thus making potential absences explicit.
Option<T> can be either:
Some<T>: Represents the presence of a value.None: Represents the absence of a value.Noneis a singleton instance. All operations or functions that result in an "empty" option will return this exact instance.
import { Some, None, Option } from "@ghaerdi/rustify"; // Assuming published package, or adjust path e.g. "./src"
const someValue: Option<number> = Some(10);
const noValue: Option<number> = None; // None is a singleton
// From potentially null/undefined values
function findConfig(key: string): { value: string } | undefined {
// ... some logic
if (key === "timeout") return { value: "3000" };
return undefined;
}
const timeoutConfig: Option<{ value: string }> = Option.fromNullable(() => findConfig("timeout"));
const missingConfig: Option<{ value: string }> = Option.fromNullable(() => findConfig("missing"));
console.log(timeoutConfig.unwrapOr({ value: "default" })); // { value: "3000" }
console.log(missingConfig.unwrapOr({ value: "default" })); // { value: "default" }
console.log(missingConfig.isNone()); // true, because findConfig("missing") returns undefinedOptions provide a variety of methods to work with potential values safely. You can check if an option is None by using myOption.isNone().
// Unwrapping (getting the value)
const val = Some(5).unwrapOr(0); // val is 5
const valOr = None().unwrapOr(0); // valOr is 0
console.log(`val: ${val}, valOr: ${valOr}`);
const valElse = None().unwrapOrElse(() => Math.random()); // valElse is a random number
console.log(`valElse: ${valElse}`);
// Expect (unwrap with a custom error if None)
// Some("data").expect("Data should be present"); // "data"
// None().expect("Data should be present"); // Throws Error: "Data should be present"
// console.log(Some("data").expect("Data should be present"));
// try { None().expect("Data should be present"); } catch (e:any) { console.error(e.message); }
// Mapping
const lengthOpt = Some("hello").map(s => s.length); // Some(5)
const noLengthOpt = None().map((s: string) => s.length); // None instance
console.log(`lengthOpt: ${lengthOpt.unwrapOr(-1)}, noLengthOpt is None: ${noLengthOpt.isNone()}`);
console.log(`Is noLengthOpt actually None? ${noLengthOpt.isNone()}`); // true
// Chaining (andThen / flatMap)
const parsed = Some("5").andThen(s => {
const num = parseInt(s, 10);
return isNaN(num) ? None : Some(num); // Return None singleton
}); // Some(5)
console.log(`parsed: ${parsed.unwrapOr(-1)}`);
const notParsed = Some("abc").andThen(s => {
const num = parseInt(s, 10);
return isNaN(num) ? None : Some(num); // Return None singleton
}); // None
console.log(`notParsed is None: ${notParsed.isNone()}`);
console.log(`Is notParsed actually None? ${notParsed.isNone()}`); // true
// Matching
const optionNumber = Some(42);
const message = optionNumber.match({
Some: value => `The number is: ${value}`,
None: () => "No number provided" // Handler for the None singleton
}); // "The number is: 42"
console.log(message);
const noOptionNumber: Option<number> = None; // Assign the None singleton
const noMessage = noOptionNumber.match({
Some: value => `The number is: ${value}`,
None: () => "No number provided"
}); // "No number provided"
console.log(noMessage);
// Direct comparison with None
if (noOptionNumber.isNone()) {
console.log("noOptionNumber is indeed the None singleton.");
}The Option type helps avoid common errors like TypeError: Cannot read property '...' of null/undefined by making the absence of a value an explicit state that must be handled. Use .isNone() method to check for None values.
A full API overview for Option would be similar to Result's, including methods like:
isSome, isNone, isSomeAnd, expect, unwrap, unwrapOr, unwrapOrElse, map, mapOr, mapOrElse, inspect, and, andThen (flatMap), or, orElse, xor, cloned, zip, zipWith, and match. Refer to the source code or generated documentation for exhaustive details on each method's behavior and signature.
This project uses Bun.
- Install Dependencies:
bun install
- Type Checking:
bun run check --watch
- Run Tests:
bun test --watch
Contributions welcome! Please submit issues and pull requests.
- Fork the repository.
- Create your feature branch.
- Commit your changes.
- Push to the branch.
- Open a Pull Request.
MIT License - see the LICENSE file for details.