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

ppipe

v3.2.0

Published

Strictly-typed piping without the operator support

Readme

PPIPE

build Coverage Status npm npm license

Strictly-typed pipes for values through functions, an alternative to using the proposed pipe operator ( |> ) for ES.

Version 3.0 is a complete TypeScript rewrite with maximum type safety - no any in the public API, full IDE autocomplete support, and correct type inference throughout the chain.

Installation

npm install ppipe

Quick Start

import ppipe, { _ } from 'ppipe';

const add = (x: number, y: number) => x + y;
const square = (x: number) => x * x;
const divide = (x: number, y: number) => x / y;
const double = (x: number) => x * 2;

// Basic piping
const result = ppipe(1)
  .pipe(add, _, 1)      // 2
  .pipe(double)         // 4
  .pipe(square)         // 16
  .pipe(divide, _, 8)   // 2
  .pipe(add, _, 1)      // 3
  .value;

console.log(result); // 3

Features

Basic Piping

Chain functions together, passing the result of each to the next:

ppipe('hello')
  .pipe(s => s.toUpperCase())
  .pipe(s => s + '!')
  .value; // 'HELLO!'

Placeholder Positioning

Use _ to control where the piped value is inserted:

const _ = ppipe._;

// Value inserted at placeholder position
ppipe(10)
  .pipe(divide, _, 2)   // divide(10, 2) = 5
  .value;

// Without placeholder, value is appended at the end
ppipe(10)
  .pipe(divide, 100)    // divide(100, 10) = 10
  .value;

// Multiple placeholders insert the same value multiple times
ppipe(5)
  .pipe((a, b) => a + b, _, _)  // 5 + 5 = 10
  .value;

Async/Promise Support

Promises are automatically handled - the chain waits for resolution and passes the unwrapped value to the next function:

async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

const userName = await ppipe(1)
  .pipe(fetchUser)
  .pipe(user => user.name)
  .pipe(name => name.toUpperCase());

// Or use .then()/.catch()
ppipe(1)
  .pipe(fetchUser)
  .pipe(user => user.name)
  .then(name => console.log(name))
  .catch(err => console.error(err));

Value Extraction

Get the current value with .value (or .val):

// Sync value
const num = ppipe(5).pipe(x => x * 2).value; // 10

// Async value (returns Promise)
const asyncNum = await ppipe(Promise.resolve(5)).pipe(x => x * 2).value;

Typed Extensions

Create reusable pipe extensions with full type inference:

const mathPipe = ppipe.extend({
  double: (x: number) => x * 2,
  square: (x: number) => x * x,
  add: (x: number, y: number) => x + y,
});

const result = mathPipe(5)
  .double()      // 10 - return type inferred as number
  .square()      // 100
  .add(5)        // 105
  .value;

// Extensions can be chained
const extendedPipe = mathPipe.extend({
  stringify: (x: number) => String(x),
});

const str = extendedPipe(5)
  .double()
  .stringify()   // '10' - return type inferred as string
  .value;

Generic Pass-Through Extensions

Generic identity functions like log or tap preserve the pipe's type automatically:

const pp = ppipe.extend({
  log: <T>(value: T, label?: string): T => {
    console.log(label ?? 'value:', value);
    return value;
  },
});

// Type is preserved through .log() - no type loss!
pp(8)
  .log('start')     // logs: "start: 8"
  .pipe(x => x + 3) // x is number, not unknown
  .log('end')       // logs: "end: 11"
  .value;           // 11

pp('hello')
  .log()
  .pipe(s => s.toUpperCase()) // s is string
  .value;                      // 'HELLO'

API Reference

ppipe(value)

Creates a new pipe with the given initial value.

const pipe = ppipe(initialValue);

.pipe(fn, ...args)

Pipes the current value through a function. The value is inserted at the placeholder position, or appended at the end if no placeholder is used.

pipe.pipe(fn)              // fn(value)
pipe.pipe(fn, _, arg2)     // fn(value, arg2)
pipe.pipe(fn, arg1)        // fn(arg1, value)
pipe.pipe(fn, arg1, _)     // fn(arg1, value)

.value / .val

Gets the current value from the chain. Returns a Promise if any function in the chain was async.

.then(onFulfilled?, onRejected?)

Standard Promise then interface. Always available for consistent async handling.

.catch(onRejected?)

Standard Promise catch interface. Always available for consistent async handling.

ppipe._

The placeholder symbol for argument positioning.

ppipe.extend(extensions)

Creates a new ppipe factory with additional methods:

const extended = ppipe.extend({
  methodName: (value, ...args) => result,
});

Extension functions receive the piped value as their first argument.

Migration from v2.x

Version 3.0 is a TypeScript rewrite that prioritizes type safety. Some dynamic features that couldn't be strictly typed have been removed:

Removed Features

| Feature | v2.x | v3.x Alternative | |---------|------|------------------| | Deep property access | _.a.b.c | .pipe(x => x.a.b.c) | | Array spreading | ..._ | .pipe(arr => fn(...arr)) | | Direct method access | .map(fn) | .pipe(arr => arr.map(fn)) | | Context binding | .with(ctx) | .pipe(fn.bind(ctx)) | | Callable syntax | ppipe(val)(fn) | ppipe(val).pipe(fn) |

Why These Changes?

These features relied on Proxy magic that returned any types, breaking TypeScript's ability to infer types correctly. The v3.x API ensures:

  • Full IDE autocomplete support
  • Correct type inference throughout the chain
  • No any types in the public API
  • Compile-time error detection

Type Safety

ppipe v3.x provides complete type inference with arity checking - passing extra arguments to functions that don't expect them produces compile-time errors:

// Types are inferred correctly through the chain
const result = ppipe(5)
  .pipe(x => x * 2)           // Pipe<number>
  .pipe(x => x.toString())    // Pipe<string>
  .pipe(x => x.length)        // Pipe<number>
  .value;                     // number

// Async types are tracked
const asyncResult = ppipe(Promise.resolve(5))
  .pipe(x => x * 2)           // Pipe<number, async=true>
  .value;                     // Promise<number>

// Extension return types are inferred
const myPipe = ppipe.extend({
  toArray: <T>(x: T) => [x],
});

myPipe(5).toArray().value;    // number[]

// Generic identity extensions preserve the pipe's type
const debugPipe = ppipe.extend({
  log: <T>(value: T): T => { console.log(value); return value; },
});

debugPipe(5).log().pipe(x => x * 2).value;  // x is number, result is number

Arity Checking

Functions are checked to ensure they receive the correct number of arguments:

const subtract = (a: number, b: number) => a - b;

// ✓ Correct - 2-param function with 2 args
ppipe(10).pipe(subtract, _, 3).value;  // 7

// ✗ Error - 2-param function with 4 args
ppipe(10).pipe(subtract, _, 3, 5, 10).value;
// Type error: Property 'value' does not exist on type 'never'

Type Safety Strengths

  • Full inference for lambdas - Untyped lambdas get correct types for 1-4 arguments
  • Arity mismatch detection - Extra arguments to named functions produce compile errors
  • Async tracking - The type system tracks whether the chain contains any async operations
  • Extension type preservation - Generic identity extensions (like log) preserve the pipe's type
  • No any in public API - Complete type safety throughout

Type Safety Limitations

| Scenario | Behavior | |----------|----------| | 5+ args with untyped lambda | Requires explicit type annotations | | Arity errors | Appear on .value access, not at .pipe() call | | Variadic functions | Allowed through arity check (by design) |

// 5+ args needs type annotations
ppipe(1).pipe(
  (a: number, b: string, c: boolean, d: number, e: string) => a + d,
  _, "x", true, 4, "end"
).value;  // ✓ Works

ppipe(1).pipe(
  (a, b, c, d, e) => a,  // ✗ 'a' will be 'never' - use typed lambda
  _, "x", true, 4, "end"
);

Testing

100% test coverage is maintained. To run tests:

npm install
npm test

Contributing

See CONTRIBUTING.

Changelog

See CHANGELOG.md for version history.

License

ISC