DEV Community

Caleb Weeks
Caleb Weeks

Posted on

Fluent API for piping standalone functions in JavaScript

Today (October 8, 2025), there is a post called Working pipe operator today in pure JavaScript on Hacker News. It's a really neat concept that uses type coercion to use the bitwise OR (|) operator. I really like the idea and how similar it looks like the widely used pipe operator (|>).

The one downside of this approach is that each function that you want to pipe needs to be wrapped in an asPipe function. If you're willing to live with this, I came up with another JavaScript hack that uses proxies to call functions as if they are methods on the calling object. The object gets passed in as the first argument of the passed in function.

First, we check if the property is a method on the target object. If it is, we simply call the method. Then we check if there is a function with the same name as the property in the provided functions and call it accordingly. Then we fall back to a regular property getter.

I like the fluent API that feels more like idiomatic JavaScript. There is also a possibility of making this work with TypeScript. I think it should be possible to make the passed-in functions appear as methods on the target object as you type in your editor.

const chainWith = (fns) => (obj) => {
  const isFunction = (fn) => typeof fn === "function";
  const chainify = (obj) => {
    if (typeof obj === "object" && obj !== null) {
      return new Proxy(obj, {
        get: (target, prop) => {
          switch (true) {
            case isFunction(target[prop]):
              return (...args) => chainify(target[prop].apply(target, args));
            case isFunction(fns[prop]):
              return (...args) =>
                chainify(fns[prop].apply(target, [target, ...args]));
            default:
              return target[prop];
          }
        },
      });
    }
    return obj;
  };
  return chainify(obj);
};

const shuffle = (arr) => arr.sort(() => Math.random() - 0.5);
const zipWith = (a, b, fn) => a.slice(0, Math.min(a.length, b.length)).map((x, i) => fn(x, b[i]));
const log = (arr) => {
  console.log(arr);
  return arr;
};

const chain = chainWith({shuffle, zipWith, log});

chain([1, 2, 3, 4, 5, 6, 7, 8, 9])
  .map((i) => i + 10)
  .log() // [ 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
  .shuffle()
  .log() // e.g. [ 16, 15, 11, 19, 12, 13, 18, 14, 17 ]
  .zipWith(["a", "b", "c", "d", "e"], (a, b) => a + b)
  .log() // e.g. [ '16a', '15b', '11c', '19d', '12e' ]
  [0]; // e.g. '16a'
Enter fullscreen mode Exit fullscreen mode

Of course, you could just use a simple pipe function like so...

const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

const pipeline = pipe(
  (x) => x.map((i) => i + 10),
  log, // [ 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
  shuffle,
  log, // e.g. [ 16, 15, 11, 19, 12, 13, 18, 14, 17 ]
  (arr) => zipWith(arr, ["a", "b", "c", "d", "e"], (a, b) => a + b),
  log // e.g. [ '16a', '15b', '11c', '19d', '12e' ]
)

pipeline([1, 2, 3, 4, 5, 6, 7, 8, 9]);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)