npm i @graficos/pipe-js
Pipes are a pattern of function composition. They enable you to do several transformations to a value in 1 step.
You pass a number of functions and a parameter that will be consumed by these functions, so you can make some transformations to this value.
For example, let's imagine this scenario, where you're applying the result of a function to another function:
var result = f(g(x))
I bet you have seen it multiple times. And something even more scary:
var result = f(g(h(i(j(x))))) // 🤔
How are you supposed to read or even debug that? This is where pipes are useful:
// The first example would be the same as:
var result = pipe(g, f)(x)
// and the second example:
var result = pipe(j, i, h, g, f)(x)
You'll notice that with pipe, we read the transformations left-to-right (which was the same as "from the inside to the outside" in the initial examples), which improves the readability.
Don't be scared about the (...)(x)
syntax, that's how you call "a function that returns a function", which is what
pipe
is. It's also called a "Higher Order Function", because you pass functions as parameters.
If you need to see what's happening on each step, you can create what we usually call a tap
function ("tap", "pipe",
...see the correlation? ;)). Imagine we have the following example:
import { pipe } from '@graficos/pipe-js'
// `formatText` is the combination of other "transformations" applied sequentially.
const formatText = pipe(removeHTMLTags, formatAuthorInfo, addFooterText)
const myFormattedText = formatText(sourceText)
We could debug each step with the tap
function:
import { pipe } from '@graficos/pipe-js'
const tap = (currentValue) => {
console.log(currentValue)
return currentValue // remember, all your functions must return the value to be passed to the next function
}
// `formatText` is the combination of other "transformations" applied sequentially.
const formatText = pipe(
removeHTMLTags,
tap,
formatAuthorInfo,
tap,
addFooterText
)
const myFormattedText = formatText(sourceText)
console.log({ myFormattedText })
That's why pipe-js
comes also with an asyncPipe
method (tree-shakeable if you don't use it).
Example:
import { asyncPipe } from '@graficos/pipe-js'
const someAsyncFunction = (value) =>
new Promise((resolve) => resolve(value + 2))
const someOtherAsyncOperation = (value) =>
new Promise((resolve) => resolve(value * 3))
const result = await asyncPipe(someAsyncFunction, someOtherAsyncOperation)(1) // -> result === 9
Just remember that just like pipe
always returned a function, asyncPipe
will always return a Promise
. You, you need to uwrap the value that it'll return with async
/await
or using .then()
.
You can see some tests to see more examples in /src (the *.spec.js
files).
Another example would be "composable derived data". Sometimes they are called selectors or "computed properties":
const addHello = (value) => `Hello, ${value}`
const toUpperCase = (value) => String.prototype.toUpperCase.call(value)
const person = {
name: 'Paul',
getGreet() {
return pipe(addHello, toUpperCase)(this.name)
},
}
console.log(person.getGreet()) // -> HELLO, PAUL
person.name = 'Hello'
console.log(person.getGreet()) // -> HELLO, HELLO
In this example, any time name
changes, the derived state will be updated (as it is a function invoked everytime with the current this.name
).
-
It's done in JS and using JSDoc, so you'll have proper autocompletion in your editor.
-
You can use it both in the browser and in node, as it is exported as UMD, ESM and CJS.
- A simple explanation of functional pipe in JavaScript by Ben Lesh
- What is Function Composition? by Eric Elliott