Version 0.10.0
Inspired by functional programming, FlowSure provides tools like ok()
, maybe()
, result()
, task()
and flow()
to simplify complex workflows. Here's a quick example:
import { ok, result } from "@efflore/flow-sure";
ok(5)
.map(x => x * 2)
.filter(x => x > 5)
.match({
Ok: value => console.log("Success:", value),
Nil: () => console.warn("No value passed the filter"),
Err: error => console.error("Error:", error.message),
});
result(() => JSON.parse("invalid json"))
.match({
Ok: value => console.log("Parsed:", value),
Err: error => console.error("Failed to parse:", error.message),
});
- Simplify Error Handling: Capture and propagate errors elegantly with
Result
types. - Composable Flows: Chain both sync and async functions seamlessly using monadic methods.
- Declarative Control: Use
flow()
to build clear, predictable pipelines. - Immutability Assurance: Safely wrap and clone mutable objects to avoid unintended side effects.
FlowSure is very lightweight: around 1kB gzipped.
# with npm
npm install @efflore/flow-sure
# or with bun
bun add @efflore/flow-sure
FlowSure's Result
types (Ok
, Err
, Nil
) let you manage value transformations and error handling with .map()
, .chain()
, .filter()
, .guard()
, .or()
and .catch()
methods. The match()
method allows for pattern matching according to the Result
type. Finally, get()
will return the value (if Ok
) or undefined
(if Nil
) or rethow the catched Error
(if Err
).
import { ok } from "@efflore/flow-sure";
ok(5).map(x => x * 2).filter(x => x > 5).match({
Ok: value => console.log("Transformed Value:", value),
Nil: () => console.warn("Value didn't meet filter criteria"),
Err: error => console.error("Error:", error.message)
});
Method | Ok<T> |
Nil |
Err<E extends Error> |
Argument Type | Return Type |
---|---|---|---|---|---|
.map() |
Yes | No-op | No-op | (value: T) => U |
Ok<U> , Nil or Err<E> |
.chain() |
Yes | No-op | No-op | (value: T) => Result<U> |
Result<U> |
.await() |
Yes | No-op | No-op | async (value: T) => Result<U> |
Promise<Result<U>> |
.filter() |
Yes | No-op | Converts to Nil |
(value: T) => boolean |
Maybe<T> |
.guard() |
Yes | No-op | Converts to Nil |
(value: T) => value is U |
Maybe<T> |
.or() |
No-op | Yes | Yes | () => T | undefined |
Ok<T> or Maybe<T> |
.catch() |
No-op | No-op | Yes | (error: E) => Result<T> |
Result<T> |
.match() |
Yes | Yes | Yes | (value: T) => any , () => any or (error: E) => any |
any |
.get() |
Returns value | Returns undefined |
Throws error | -- | T , void or E |
.map()
: Transforms the value if it exists (Ok
);Nil
andErr
remain unchanged..chain()
: Chains a function that returns a newResult
, only applies toOk
..await()
: Chains an async function that returns aPromise
for a newResult
, only applies toOk
..filter()
: FiltersOk
based on a condition; converts toNil
if the condition is not met..guard()
: Similar to.filter()
, but specifically used for type narrowing onOk
..or()
: Provides a fallback forNil
andErr
, leavingOk
unchanged..catch()
: Handles errors withinErr
types, leavingOk
andNil
unchanged..match()
: Allows pattern matching acrossOk
,Nil
, andErr
..get()
: Retrieves the contained value, returningundefined
forNil
and throwing forErr
.
Using maybe()
ensures that undefined
or null
values are handled explicitly, reducing the risk of runtime errors caused by forgotten null checks. Instead of if
statements scattered throughout your code, you can use methods like .map()
, .filter()
, and .match()
to express intent clearly.
import { maybe } from "@efflore/flow-sure";
const optionalValue = undefined; // Could also be null or an actual value
maybe(optionalValue)
.map(value => value * 2)
.filter(value => value > 5)
.match({
Ok: value => console.log("Value:", value),
Nil: () => console.warn("Value is either missing or didn't meet criteria")
});
result()
is especially useful for wrapping code that may throw unexpected errors, such as parsing user input or accessing properties on potentially null objects.
It captures exceptions and converts them into Err
values, allowing you to handle errors gracefully within the chain.
import { result } from "@efflore/flow-sure";
result(() => {
// Function that may throw an error
return JSON.parse("invalid json");
}).match({
Ok: value => console.log("Parsed JSON:", value),
Err: error => console.error("Failed to parse JSON:", error.message)
});
Use task()
to retrieve and handle a promised result, wrapping it in Result
types (Ok
, Err
, Nil
). Here's an example of how you can add retry logic for async operations:
import { task, err } from "@efflore/flow-sure";
const fetchData = async () => {
const response = await fetch('/api/data');
if (!response.ok) return err(`Failed to fetch data: ${response.statusText}`);
return response.json();
}
const retry = <T>(
fn: () => Promise<MaybeResult<T>>,
retries: number,
delay: number
) => task(fn).catch((error: Error) => {
if (retries <= 0) return err(error);
return new Promise(resolve => setTimeout(resolve, delay))
.then(() => retry(fn, retries - 1, delay * 2));
});
// 3 attempts, exponential backoff with initial 1000ms delay
const loadData = async () {
const data = await retry(fetchData, 3, 1000);
// Process data ...
}
flow()
enables you to compose a series of functions (both sync and async) into a cohesive pipeline:
import { flow } from "@efflore/flow-sure";
const processData = async () {
const result = await flow(
10,
x => x * 2,
async x => await someAsyncOperation(x)
).match({
Ok: finalValue => console.log("Result:", finalValue),
Err: error => console.error("Error:", error.message)
});
// Render data ...
}
The ok()
function wraps values to enforce immutability and prevent multiple retrievals. For mutable objects, it attempts to clone the value using structuredClone()
. This ensures that modifications to the original object do not affect the wrapped value.
Note: Some types (e.g., DOM elements, promises,
WeakMap
,WeakSet
) cannot be cloned due tostructuredClone()
limitations. In these cases,ok()
falls back to treating the value as immutable without guarantees against external modification. Document these edge cases in your codebase if immutability is critical.
The value of an Ok
instance can only be retrieved once using .get()
. Any subsequent attempts will throw a ReferenceError
. You can check if the value has already been consumed using isGone()
or the .gone
property on the instance.
import { ok } from '@efflore/flow-sure';
// Example with a mutable object
const original = { a: 1, b: 2 };
const wrapped = ok(original);
// Modifying the original object does not affect the wrapped value
original.a = 42;
console.log(wrapped.get()); // { a: 1, b: 2 } (immutable clone)
// Attempting to retrieve the value again throws a ReferenceError
try {
console.log(wrapped.get());
} catch (e) {
console.error(e.message); // "Mutable reference has already been consumed"
}
// Check if the value has been consumed
console.log(wrapped.gone); // true
FlowSure also exports the following utility functions it uses internally:
isFunction(value: unknown): value is (...args: any[]) => any
Checks if the given value is a function.
isAsyncFunction(value: unknown): value is (...args: any[]) => Promise<any>
Checks if the given value is an asynchronous function (returns a Promise
).
isDefined(value: unknown): value is NonNullable<typeof value>
Checks if the given value is neither null
nor undefined
.
isMutable(value: unknown): value is Record<PropertyKey, unknown>
Checks if the given value is a mutable object (non-null
and typeof value === "object"
).
isInstanceOf<T>(type: new (...args: any[]) => T): (value: unknown) => value is T
Creates a type guard to check if a value is an instance of a specific class or type.
isError(value: unknown): value is Error
Checks if the given value is an Error instance.
log(msg: string, logger: (...args: any[]) => void = console.log): (...args: any[]) => any
Logs a message and additional arguments using the specified logger (default: console.log). Returns the first argument for chaining.
tryClone<T>(value: T, warn = true): T
Attempts to clone a mutable object using structuredClone(). If cloning fails, logs a warning (if warn is true) and returns the original value.
wrap<T>(value: MaybeResult<T>): Result<T>
Wraps a value in a Result
container. If the value is an error, it returns Err
. If the value is undefined or null, it returns Nil
. Otherwise, it returns Ok
. Values that are already of a Result
type are not double-wrapped.
unwrap<T>(value: Result<T> | T | void): T | Error | void
Unwraps a Result
container, returning the value if it is Ok
, the error if it is Err
, or undefined if it is Nil
.