UNPKG

@efflore/flow-sure

Version:

FlowSure - a Result monad in TypeScript. Data types Ok, Nil, Err with maybe(), result(), asyncResult() and flow() functions.

255 lines (194 loc) 10.7 kB
# FlowSure 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: ```js 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), }); ``` ## Key Features - **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. ## Installation ```bash # with npm npm install @efflore/flow-sure # or with bun bun add @efflore/flow-sure ``` ## Basic Usage ### Monadic Control with Result Types 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`). ```js 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) }); ``` #### Monadic Methods Table | 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` | #### Explanation of Each Method * `.map()`: Transforms the value if it exists (`Ok`); `Nil` and `Err` remain unchanged. * `.chain()`: Chains a function that returns a new `Result`, only applies to `Ok`. * `.await()`: Chains an async function that returns a `Promise` for a new `Result`, only applies to `Ok`. * `.filter()`: Filters `Ok` based on a condition; converts to `Nil` if the condition is not met. * `.guard()`: Similar to `.filter()`, but specifically used for type narrowing on `Ok`. * `.or()`: Provides a fallback for `Nil` and `Err`, leaving `Ok` unchanged. * `.catch()`: Handles errors within `Err` types, leaving `Ok` and `Nil` unchanged. * `.match()`: Allows pattern matching across `Ok`, `Nil`, and `Err`. * `.get()`: Retrieves the contained value, returning `undefined` for `Nil` and throwing for `Err`. ### Handling Optional or Missing Values with maybe() 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. ```js 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") }); ``` ### Handling Exceptions with result() `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. ```js 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) }); ``` ### Handling Promises with task() 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: ```ts 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 ... } ``` ### Using flow() for Declarative Control `flow()` enables you to compose a series of functions (both sync and async) into a cohesive pipeline: ```js 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 ... } ``` ### Using ok() for Immutability Guarantees 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 to `structuredClone()` 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. ```js 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 ``` ### Exported Helper Functions **FlowSure** also exports the following utility functions it uses internally: ```ts isFunction(value: unknown): value is (...args: any[]) => any ``` Checks if the given value is a function. ```ts isAsyncFunction(value: unknown): value is (...args: any[]) => Promise<any> ``` Checks if the given value is an asynchronous function (returns a `Promise`). ```ts isDefined(value: unknown): value is NonNullable<typeof value> ``` Checks if the given value is neither `null` nor `undefined`. ```ts isMutable(value: unknown): value is Record<PropertyKey, unknown> ``` Checks if the given value is a mutable object (non-`null` and `typeof value === "object"`). ```ts 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. ```ts isError(value: unknown): value is Error ``` Checks if the given value is an Error instance. ```ts 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. ```ts 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. ```ts 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. ```ts 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`.