pipeout
TypeScript icon, indicating that this package has built-in type declarations

0.1.0 • Public • Published

Pipeout

logo

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.

Package Sidebar

Install

npm i pipeout

Weekly Downloads

4

Version

0.1.0

License

MIT

Unpacked Size

43.3 kB

Total Files

14

Last publish

Collaborators

  • will-wow