npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thuum/piper

v0.0.3

Published

Functional programming utilities for pipe operations and function composition with async support

Readme

@thuum/piper

Functional programming utilities for pipe operations and function composition in TypeScript.

Installation

npm install @thuum/piper
# or
bun add @thuum/piper
# or
yarn add @thuum/piper

Overview

@thuum/piper provides utilities for functional programming with support for both synchronous and asynchronous operations:

  • pipe(value) - Transform a value through a chain of synchronous functions
  • build<T>() - Compose synchronous functions into a reusable transformation pipeline
  • pipe(value) (async) - Transform values through chains that support both sync and async functions
  • build<T>() (async) - Compose functions (sync or async) into a reusable async pipeline

Sync and async utilities share the same API shape but are imported from different subpaths:

import { pipe, build } from "@thuum/piper";       // sync
import { pipe, build } from "@thuum/piper/async"; // async

All utilities provide full TypeScript type safety with automatic type inference.

Features

  • 🔗 Fluent API - Chain transformations with .pipe()
  • 🔄 Function Composition - Build reusable transformation pipelines
  • Flexible Async Support - Mix synchronous and asynchronous functions seamlessly
  • 🎯 Type Safe - Full TypeScript inference throughout the chain
  • 🪶 Lightweight - Zero dependencies
  • 📦 Dual Package - ESM and CommonJS support

API

pipe(value)

Creates a pipe that transforms a value through a sequence of synchronous functions.

Signature

function pipe<T>(value: T): ValuePipe<T>;

interface ValuePipe<T> {
  pipe<R>(fn: (x: T) => R): ValuePipe<R>;
  readonly value: T;
}

Examples

Basic arithmetic
import { pipe } from "@thuum/piper";

const { value } = pipe(1)
  .pipe((x) => x + 1)
  .pipe((x) => x * 2);

console.log(value); // 4
Parsing and normalizing user input
import { pipe } from "@thuum/piper";

const { value: slug } = pipe("  Hello World!  ")
  .pipe((s) => s.trim())
  .pipe((s) => s.toLowerCase())
  .pipe((s) => s.replace(/\s+/g, "-"))
  .pipe((s) => s.replace(/[^a-z0-9-]/g, ""));

console.log(slug); // "hello-world"
Reshaping an object through multiple steps
import { pipe } from "@thuum/piper";

const { value: greeting } = pipe({ first: "Jane", last: "Doe", age: 28 })
  .pipe((user) => ({ ...user, fullName: `${user.first} ${user.last}` }))
  .pipe((user) => `Hi ${user.fullName}, you are ${user.age} years old.`);

console.log(greeting); // "Hi Jane Doe, you are 28 years old."

Use Cases

  • One-time value transformations
  • Data processing pipelines
  • Complex calculations with intermediate steps
  • Avoiding nested function calls

build<T>()

Creates a reusable function by composing a sequence of synchronous transformations.

Signature

function build<X>(): FunctionPipe<X, X>;

interface FunctionPipe<X, Y> {
  pipe<Z>(fn: (y: Y) => Z): FunctionPipe<X, Z>;
  fn: (x: X) => Y;
}

Examples

Basic composition
import { build } from "@thuum/piper";

const { fn } = build<number>()
  .pipe((x) => x + 1)
  .pipe((x) => x * 2);

console.log(fn(1)); // 4
console.log(fn(2)); // 6
Building a URL slug generator
import { build } from "@thuum/piper";

const { fn: toSlug } = build<string>()
  .pipe((s) => s.trim())
  .pipe((s) => s.toLowerCase())
  .pipe((s) => s.replace(/\s+/g, "-"))
  .pipe((s) => s.replace(/[^a-z0-9-]/g, ""));

console.log(toSlug("  Blog Post Title! ")); // "blog-post-title"
console.log(toSlug("Another One  ")); // "another-one"
Domain-specific formatter
import { build } from "@thuum/piper";

const { fn: formatPrice } = build<number>()
  .pipe((cents) => cents / 100)
  .pipe((dollars) => dollars.toFixed(2))
  .pipe((str) => `$${str}`);

console.log(formatPrice(1999)); // "$19.99"
console.log(formatPrice(50)); // "$0.50"

Use Cases

  • Creating reusable transformation functions
  • Building domain-specific utilities
  • Function composition patterns
  • Reducing code duplication

Async Support

The async variants support both synchronous and asynchronous functions in the same pipeline using the MaybePromise<T> type, which allows seamless mixing of sync and async operations.

Async utilities are imported from the @thuum/piper/async subpath:

import { pipe, build } from "@thuum/piper/async";

pipe(value) (async)

Transform values through a chain that accepts both sync and async functions. The value itself can also be a Promise.

Signature

type MaybePromise<T> = T | Promise<T>;

function pipe<T>(value: MaybePromise<T>): ValuePipe<T>;

interface ValuePipe<T> {
  pipe<R>(fn: (x: T) => MaybePromise<R>): ValuePipe<R>;
  readonly value: MaybePromise<T>;
}

Examples

Fetching and transforming API data
import { pipe } from "@thuum/piper/async";

const { value: userName } = pipe(fetch("/api/user/1"))
  .pipe((res) => res.json())
  .pipe((data) => data.name)
  .pipe((name) => name.toUpperCase());

const result = await userName; // "ALICE"
Mixing sync and async steps
import { pipe } from "@thuum/piper/async";

const { value } = pipe(5)
  .pipe((x) => x * 2) // sync
  .pipe(async (x) => await lookupLabel(x)) // async I/O
  .pipe((label) => label.trim()); // sync

const result = await value;
Starting from an existing Promise
import { pipe } from "@thuum/piper/async";

const { value } = pipe(Promise.resolve("raw input"))
  .pipe((s) => s.trim())
  .pipe(async (s) => await translate(s, "en", "fr"));

const result = await value;
Error propagation — rejected promises short-circuit the chain
import { pipe } from "@thuum/piper/async";

const { value } = pipe(1)
  .pipe(() => Promise.reject(new Error("boom")))
  .pipe((x) => x + 1); // never called

await value; // throws Error("boom")

build<T>() (async)

Compose a reusable function from both sync and async transformations.

Signature

type MaybePromise<T> = T | Promise<T>;

function build<X>(): FunctionPipe<X, X>;

interface FunctionPipe<X, Y> {
  pipe<Z>(fn: (y: Y) => MaybePromise<Z>): FunctionPipe<X, Z>;
  fn: (x: X) => MaybePromise<Y>;
}

Examples

Composing an API client handler
import { build } from "@thuum/piper/async";

const { fn: getUser } = build<number>()
  .pipe(async (id) => fetch(`/api/users/${id}`))
  .pipe((res) => res.json())
  .pipe((data) => ({ id: data.id, name: data.name }));

const user = await getUser(42);
console.log(user.name); // "Alice"
Reusable file processing pipeline
import { build } from "@thuum/piper/async";

const { fn: processFile } = build<string>()
  .pipe(async (path) => await readFile(path, "utf-8"))
  .pipe((content) => content.split("\n"))
  .pipe((lines) => lines.filter((line) => line.trim() !== ""))
  .pipe((lines) => lines.length);

const count = await processFile("./data.txt");
console.log(count); // 150
Mixed sync/async with error short-circuiting
import { build } from "@thuum/piper/async";

const { fn: safeDivide } = build<{ a: number; b: number }>()
  .pipe(({ a, b }) => {
    if (b === 0) throw new Error("Division by zero");
    return a / b;
  })
  .pipe(async (result) => await saveToAuditLog(result))
  .pipe((saved) => saved.id);

const id = await safeDivide({ a: 10, b: 2 });

Why MaybePromise?

The MaybePromise<T> type allows you to:

  • Mix function types: Use both sync and async functions in the same pipeline
  • Optimize performance: Use sync functions when possible, async only when needed
  • Simplify code: No need to wrap sync functions in Promise.resolve()
  • Type safety: TypeScript ensures the entire pipeline is correctly typed

Advanced Examples

Complex Data Transformation

import { pipe } from "@thuum/piper";

interface User {
  name: string;
  age: number;
  email: string;
}

const { value: summary } = pipe({
  name: "Alice",
  age: 30,
  email: "[email protected]",
})
  .pipe((user) => ({ ...user, age: user.age + 1 }))
  .pipe((user) => ({
    fullName: user.name.toUpperCase(),
    isAdult: user.age >= 18,
    contact: user.email,
  }))
  .pipe((data) => `${data.fullName} (Adult: ${data.isAdult})`);

console.log(summary); // "ALICE (Adult: true)"

Building Utility Functions

import { build } from "@thuum/piper";

// Create a string sanitizer
const { fn: sanitize } = build<string>()
  .pipe((str) => str.trim())
  .pipe((str) => str.toLowerCase())
  .pipe((str) => str.replace(/\s+/g, "-"))
  .pipe((str) => str.replace(/[^a-z0-9-]/g, ""));

console.log(sanitize("  Hello World! 123  ")); // "hello-world-123"
console.log(sanitize("TypeScript & Node.js")); // "typescript-nodejs"

Async API Workflow with Mixed Sync/Async

import { build } from "@thuum/piper/async";

interface ApiResponse {
  data: { id: number; value: string }[];
}

const { fn: fetchUserData } = build<number>()
  .pipe(async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json() as Promise<ApiResponse>;
  })
  .pipe((response) => response.data) // sync
  .pipe((data) => data.map((item) => item.value)) // sync
  .pipe((values) => values.join(", ")); // sync

const result = await fetchUserData(42);

Error Handling with Async Pipeline

import { build } from "@thuum/piper/async";

const { fn: safeFetch } = build<string>()
  .pipe(async (url) => {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    } catch (error) {
      return { error: error.message };
    }
  })
  .pipe((result) => {
    if ("error" in result) {
      return `Error: ${result.error}`;
    }
    return `Success: ${JSON.stringify(result)}`;
  });

const result = await safeFetch("https://api.example.com/data");

Combining with @thuum/decor

import { build } from "@thuum/piper";
import { attempt } from "@thuum/decor";

const { fn: safeDivide } = build<{ a: number; b: number }>()
  .pipe(({ a, b }) => {
    if (b === 0) throw new Error("Division by zero");
    return a / b;
  })
  .pipe((result) => Math.round(result * 100) / 100);

// Wrap with attempt for error handling
const divideSafe = attempt(safeDivide);

const result1 = divideSafe({ a: 10, b: 2 });
console.log(result1); // { value: 5, error: undefined }

const result2 = divideSafe({ a: 10, b: 0 });
console.log(result2); // { value: undefined, error: Error("Division by zero") }

Real-World Example: Data Processing Pipeline

import { pipe } from "@thuum/piper/async";

interface RawData {
  timestamp: string;
  value: string;
}

interface ProcessedData {
  date: Date;
  value: number;
  category: string;
}

const { value } = pipe<RawData[]>(fetch("/api/data").then((r) => r.json()))
  .pipe((data) => data.filter((item) => item.value !== null)) // sync
  .pipe(async (data) => {
    // Simulate async validation
    await new Promise((resolve) => setTimeout(resolve, 100));
    return data;
  })
  .pipe((data) =>
    data.map((item) => ({
      // sync
      date: new Date(item.timestamp),
      value: parseFloat(item.value),
      category: parseFloat(item.value) > 100 ? "high" : "low",
    })),
  )
  .pipe((data) =>
    data.sort(
      (
        a,
        b, // sync
      ) => b.date.getTime() - a.date.getTime(),
    ),
  );

const processed: ProcessedData[] = await value;

TypeScript Support

Full type inference throughout the pipeline:

import { pipe, build } from "@thuum/piper";
import { pipe as asyncPipe, build as asyncBuild } from "@thuum/piper/async";

// Sync pipe - types are automatically inferred at each step
const { value } = pipe(42)
  .pipe((x) => x.toString()) // x: number, returns: string
  .pipe((x) => x.length) // x: string, returns: number
  .pipe((x) => x > 1); // x: number, returns: boolean
// value is inferred as boolean

// Sync build
const { fn } = build<string>()
  .pipe((x) => x.split(",")) // returns: string[]
  .pipe((x) => x.length) // returns: number
  .pipe((x) => x * 2); // returns: number
// fn is inferred as (x: string) => number

// Async pipe with MaybePromise
const { value: asyncValue } = asyncPipe(42)
  .pipe((x) => x + 1) // sync: returns number
  .pipe(async (x) => x * 2) // async: returns Promise<number>
  .pipe((x) => x.toString()); // sync: returns string
// asyncValue is inferred as MaybePromise<string>

// Async build with MaybePromise
const { fn: asyncFn } = asyncBuild<number>()
  .pipe(async (x) => x + 1) // returns: MaybePromise<number>
  .pipe((x) => x * 2) // returns: MaybePromise<number>
  .pipe((x) => x.toString()); // returns: MaybePromise<string>
// asyncFn is inferred as (x: number) => MaybePromise<string>

Comparison with Native Methods

Without Piper

const result = Math.round((parseInt(input.trim().toLowerCase()) + 10) * 2);

With Piper

import { pipe } from "@thuum/piper";

const { value: result } = pipe(input)
  .pipe((s) => s.trim())
  .pipe((s) => s.toLowerCase())
  .pipe((s) => parseInt(s))
  .pipe((n) => n + 10)
  .pipe((n) => n * 2)
  .pipe((n) => Math.round(n));

Benefits:

  • More readable left-to-right flow
  • Easier to debug (inspect intermediate values)
  • Simple to add/remove transformation steps
  • Type safety at each step
  • Clear separation of concerns

Package Exports

// Sync exports
import { pipe, build } from "@thuum/piper";

// Async exports (from subpath)
import { pipe, build } from "@thuum/piper/async";

// To use both in the same file, alias the async imports
import { pipe, build } from "@thuum/piper";
import { pipe as asyncPipe, build as asyncBuild } from "@thuum/piper/async";

License

ISC

Contributing

Contributions are welcome! Please see the main Thuum repository for contribution guidelines.

Related Packages