pieper
v0.0.7
Published
🥧 A tiny, typesafe, asynchronous pipeline for functional programming in TypeScript.
Downloads
535
Readme
What Does It Do?
Pieper allows you to build complex, sequential data flows with a fluent, chainable API. It's designed to be async-native from the ground up, so you can mix synchronous and asynchronous operations seamlessly without cluttering your code with await at every step.
import { Pieper, type SafeResult } from "pieper";
async function slugify(
title: string | null | undefined
): Promise<SafeResult<string>> {
const pipe = Pieper.of(title)
.assert(
(s): s is string => typeof s === "string" && s.trim().length !== 0,
"Input title cannot be empty"
)
.log("1. Original:")
.map((s) => s.toLowerCase())
.map((s) => s.normalize("NFD"))
.map((s) => s.replace(/[\u0300-\u036f]/g, ""))
.log("2. Normalized/Lowercased:")
.map((s) => s.replace(/[^a-z0-9]+/g, "-"))
.log("3. Hyphenated:")
.map((s) => s.replace(/^-+|-+$/g, ""))
.log("4. Trimmed:")
.ifElse(
(s) => s.length > 0,
(s) => s,
() => "n-a"
);
return pipe.runSafe();
}Features
- Fluent & Chainable: A clean, easy-to-read API (
.map().tap().if()...). - Fully Typesafe: TypeScript tracks the data type as it changes through the pipe.
- Async-Native: The entire pipeline is built on Promises. Any step can be
async. - Robust Error Handling: Use
.catch()for internal recovery or.runSafe()for external, throw-free error handling. - Zero Dependencies: A single, lightweight class.
Installation
npm install pieper
# or
yarn add pieperBasic Usage
Import Pieper and create a new pipeline using Pieper.of():
import { Pieper } from "pieper";
async function main() {
const result = await Pieper.of(5)
.map((x) => x + 2)
.tap((x) => console.log("Current value:", x))
.if(
(x) => x > 5,
(x) => x * 10
)
.log("After .if:")
.catch((err) => {
console.error("Oops:", err.message);
return 0; // Recover with a default value
})
.run();
console.log("Final result:", result); // Final result: 70
}
main();API Reference
Constructors
Pieper.of<T>(value: T | Promise<T>): Pieper<T>
Creates a new pipeline from a value or a promise.
const p1 = Pieper.of(10);
const p2 = Pieper.of(Promise.resolve(10));Pieper.from<T>(fn: () => T | Promise<T>): Pieper<T>
Creates a new pipeline by executing a function. This is useful for deferring the creation of the initial value.
const p = Pieper.from(() => heavyComputation());
const pAsync = Pieper.from(() => fs.promises.readFile("..."));Transformation
.map<R>(fn: (value: T) => R | Promise<R>): Pieper<R>
Transforms the value in the pipe from T to R. The fn can be sync or async.
Pieper.of(5)
.map((x) => x.toString()) // Pieper<string>
.map(async (str) => `Value is ${str}`); // Pieper<string>.if<R>(condition: (value: T) => boolean, thenFn: (value: T) => R | Promise<R>): Pieper<T | R>
Conditionally transforms the value only if the condition is true. If false, the original value is passed through.
Pieper.of(5).if(
(x) => x < 10,
(x) => x * 100
); // -> 500
Pieper.of(20).if(
(x) => x < 10,
(x) => x * 100
); // -> 20 (passes through).ifElse<R1, R2>(condition: (value: T) => boolean, thenFn: (value: T) => R1, elseFn: (value: T) => R2): Pieper<R1 | R2>
Transforms the value into one of two different types based on a condition.
Pieper.of(10).ifElse(
(x) => x > 5, // condition
(x) => "is-large", // thenFn
(x) => "is-small" // elseFn
); // Pieper<string> -> "is-large"Side Effects
.tap(fn: (value: T) => void | Promise<void>): Pieper<T>
Performs a side effect (like logging) without changing the value.
Pieper.of(10)
.tap((x) => console.log(x))
.map((x) => x * 2);.log(message?: string): Pieper<T>
A convenience helper for .tap() to quickly log the current value.
Pieper.of({ id: 1, name: "Test" }).log("User data:");
// Logs: "User data:" { id: 1, name: "Test" }Validation
.assert(predicate: (value: T) => boolean, errorMessage: string | Error): Pieper<T>
Enforces a contract. If the predicate is false, the pipe fails (rejects) with the given error.
Pieper.of(email).assert((str) => str.includes("@"), "Invalid email");Error Handling
.catch<R>(fn: (error: any) => R | Promise<R>): Pieper<T | R>
Recovers from a failed pipeline. The fn receives the error and must return a new "success" value.
Pieper.from(() => {
throw new Error("Kaboom!");
})
.map((x) => x * 2)
.catch((err) => {
console.error(err.message); // "Kaboom!"
return 0; // Recover with 0
}); // Pieper<number>.finally(fn: () => void | Promise<void>): Pieper<T>
Runs a function when the pipe settles (either on success or failure).
Pieper.of(db.connect())
.map((conn) => conn.query("..."))
.finally(() => db.close());Executors (Getting the Value Out)
The pipeline is lazy and does nothing until you call an executor.
.run(): Promise<T>
Executes the pipeline and returns a promise for the final value. This promise will reject if any step fails.
try {
const result = await Pieper.of(10).run();
console.log(result); // 10
} catch (e) {
console.error("Failed:", e);
}.runSafe(): Promise<SafeResult<T>>
Executes the pipeline and never throws. It always resolves with a SafeResult object:
{ ok: true, value: T }on success.{ ok: false, error: any }on failure.
This is the recommended way to handle results in a functional style.
import { Pieper, type SafeResult } from "pieper";
const result = await Pieper.of("not-an-email")
.assert((x) => x.includes("@"), "Bad email")
.runSafe();
if (result.ok) {
// result.value is string
console.log("Success:", result.value);
} else {
// result.error is the Error("Bad email")
console.error("Failure:", result.error.message);
}.runAndForget(): void
Executes the pipeline and ignores the result. Any unhandled errors will be logged to the console. Use this for "fire-and-forget" operations.
Pieper.of("Data")
.map((data) => analytics.track(data))
.runAndForget();