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 🙏

© 2024 – Pkg Stats / Ryan Hefner

pipeout

v0.1.0

Published

Error handling

Downloads

529

Readme

Pipeout

A library for piping values through transformations, in a type-safe way.

npm package codecov David Dependency Status

It's like pipe from lodash and Ramda, but with partial application for better type safety. It also includes an asynchronous pipe function for dealing with promises.

Installation

npm i pipeout --save
# or
yarn add pipeout
import { pipe, pipeA, pipe, pipeA } from "pipeout";

Examples

For these examples, we'll imagine we're building a set of functions for counting and filtering marbles. For the full example, see readme.test.ts.

Synchronous

pipe and pipe work with synchronous data (which is not wrapped in a promise).

Here's an example trying to find how many red marbles are in a list.

pipe

pipe is a basic pipe, like |> in Haskell or Elixir. The first value passed in is a value to transform. After that you can pass a series of transformer functions by using the thru method. Each one will transform the value returned from the previous function, and return an object you can call .thru on again to pipe again, or call .value() on to get the transformed value.

import { pipe } from "pipeout";

const redCount = pipe(marbles)
  .thru(filterReds)
  .thru(getLength)
  .value();

Note

Since pipe is a pretty common function name in libraries (like RxJS), pipe is aliased as pip for convenience.

import { pip } from "pipeout";`

Point-free pipe.thru

pipe in Lodash or Ramda works a little differently. Instead of passing in the value to transform immediately, it just takes a list of functions, and returns a function that will run the transformations when called. In functional programming this is called a "point-free function", and is good for situations where you're defining a function that will be run later.

If you want the same thing in pipeout, then instead of calling pipe(), call pipe.thru. You still chain together functions with .thru, but now calling the resulting function will run the pipeline.

import { pipe } from "pipeout";

const redCounter = pipe.thru(filterReds).thru(getLength);
const redCount = redCounter(marbles);

Immutable pipes

Calling .thru returns a new function, so if you have a reference to a pipe, calling .thru on it won't mutate the original function.

import { pipe } from "pipeout";

const getSmallReds = pipe.thru(onlyRed).thru(onlySmall);
const smallRedCounter = getSmallReds.thru(getLength);

const smallReds = getSmallReds(marbles);
const smallRedCount = smallRedCounter(marbles);

Asynchronous

There are also asynchronous variants, pipeA and pipeA.thru. These will always result in a promise, and will work whether your values and functions are synchronous or asynchronous.

All transformer functions should take a value, and can return a value OR a promise.

The starting value can be a promise or a value.

For this example, we'll imagine that getting the user's marbles and getting the user's favorite color are asynchronous API operations.

pipeA

import { pipeA } from "pipeout";

const redCount = await pipeA(user)
  .thru(fetchMarbles)
  .thru(filterForFavoriteColor)
  .thru(getLength)
  .value();

pipeA.thru

import { pipeA } from "pipeout";

const redCounter = pipeA
  .thru(fetchMarbles)
  .thru(filterForFavoriteColor)
  .thru(getLength);

const redCount = await redCounter(user);

Error Handling

Because pipeA chains together promises, you can handle promise errors as normal.

You can .catch errors:

const redCounter = pipeA
  .thru(fetchMarbles)
  .thru(filterForFavoriteColor)
  .thru(getLength);

const redCount = await redCounter(Promise.resolve(user));

Or handle them with async/await and try/catch:

const redCounter = pipeA
  .thru(fetchMarblesWillFail)
  .thru(filterForFavoriteColor)
  .thru(getLength);

try {
  await redCounter(Promise.resolve(user));
} catch (error) {
  expect(error).toBe("you've lost your marbles!");
}

Why another pipe function?

In JavaScript, the traditional pipe function in a variadic function that takes any number of unary transformer functions, and returns a function that pipes a value through each transformer.

It usually looks a little like this:

pipe(
  a,
  b,
  c
)(value);

That works pretty well! But creating TypeScript typings for it is a pain, as you have to declare a separate overload for every possible arity, like these Ramda types:

pipe<T1>(fn0: () => T1): () => T1;
pipe<V0, T1>(fn0: (x0: V0) => T1): (x0: V0) => T1;
pipe<V0, V1, T1>(fn0: (x0: V0, x1: V1) => T1): (x0: V0, x1: V1) => T1;
pipe<V0, V1, V2, T1>(fn0: (x0: V0, x1: V1, x2: V2) => T1): (x0: V0, x1: V1, x2: V2) => T1;
...

What a pain to maintain! Pipeout takes a different approach. pipe is mostly useful for curried functions - so why not curry pipe itself? pipeout.pipe.thru is essentially a recursive curried function. It takes a single function, and returns a version of pipe that already has the first function in memory. So you can keep calling .thru, passing in more transformers. When you're done setting up functions, you call .run with an argument, and it passing your value through all the functions.

That means we can write the same thing like this:

pipe
  .thru(a)
  .thru(b)
  .thru(c)(value);

It's type-safe, no matter how many functions you add in. And the type is nice and simple, instead of the long overloaded type from Ramda. Every call to pipe.thru just returns this same recursive type:

export interface Piper<T, U> {
  (value: T): U;
  thru: <V>(transformer: Unary<U, V>) => Piper<T, V>;
}

It describes how you can call the function to transform T to U, or call .thru to get a T to V pipeline instead. All the intermediate transformations aren't relevant, so they don't have to show up in the type. Simple!

Contributing

yarn install
yarn test
yarn lint
yarn format
yarn build

Thanks

Thanks to Ramda for their pipe function, and mostly thanks to @sidke for coming up with the original idea for Pipeout, and working through the types with me.