# Pipeline operators in JavaScript

Understand everything about the proposal that has been gaining a lot of traction in ECMAScript: pipeline operators.

- URL: https://blog.lsantos.dev/en/pipeline-operators-in-javascript/
- Published: 2022-04-04
- Updated: 2026-07-16
- Section: javascript
- Tags: javascript, ecmascript, development
- Language: en
- Author: Lucas Santos

---
**JavaScript** is always evolving and, [as](/js-tipos-proposal/) [usual](/novos-tipos-js/), I'm going to write about yet another [proposal](https://github.com/tc39/proposal-pipeline-operator#tacit-unary-function-application-syntax) that's been gaining traction in the community. The **pipeline operators**. This proposal is still at stage 1, meaning very early in the process, but it's already been dragging on for [about 6 years](https://github.com/tc39/proposal-pipeline-operator/commit/3d4e985dba6e2a5b2bdc1999268537e2071131b6). You can still test it online though, using [Babel](https://babeljs.io/repl#?browsers=&build=&builtIns=false&corejs=false&spec=false&loose=false&code_lz=MYewdgzgLgBAZgSzAQwDYwLwwNoHICyApjBAgObK4C6MAUAD4B8MApAHShjDJQAUeKCDABOhACaEhEEMATIEEagEoGzdp2588IAK4xCAW1wAaGLgAWUKAAcIALgD0D1BGRgoICGwkA3ZatY2ACsQJF5cMxUmGE5pVEI2VBAyXhYlIA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=false&presets=env%2Cstage-1&prettier=true&targets=Node-16&version=7.17.8&externalPlugins=%40babel%2Fplugin-syntax-typescript%407.16.7%2Cbabel-plugin-const-enum%401.2.0&assumptions=%7B%7D).

If you don't already know how JavaScript works and how it evolves, I invite you to watch my video explaining a bit about this topic:

![](https://www.youtube.com/watch?v=hDQu3AvvDfg)

This [isn't the first time](https://github.com/tc39/proposal-pipeline-operator#tc39-has-rejected-f-pipes-multiple-times) that pipeline operators have been suggested for the language (actually, it's the third), but this time it might be a bit different because we have another set of information we can use to complete this puzzle.

## What's the proposal

Pipeline operators translate to **flow operators**, and the idea is basically the same as the `.pipe` function we have in streams (which I already explained [here](https://imasters.com.br/back-end/streams-no-node-js-o-que-sao-streams-afinal-parte-01), [here](https://imasters.com.br/back-end/streams-no-node-js-o-que-sao-streams-afinal-parte-02) and [here](https://imasters.com.br/back-end/streams-no-node-js-o-que-sao-streams-afinal-parte-03)): essentially, they work by calling functions and passing the output of one function to the input of another, pretty similar to what bash's `|`, for example, does.

The biggest difference is that, unlike `|`, which only accepts **unary functions**, meaning functions with a single input parameter (like `(x) => {}`), pipe operators are supposed to accept any type of operation.

To understand a bit better how these operators work and why they were suggested for the language, we first need to understand two programming styles, two ways of writing code: **deep nesting** and **fluent interfaces**. And then know a bit about the history behind **functional languages**.

## Deep Nesting

When we talk about pipelines, we're basically talking about sequential function execution, meaning the result of a function or expression gets passed to the next one, like a cake recipe, where after each step we take what we already have and pass it to the next stage of the process until we get a final result.

A great example of this is the array's `reduce` function, which basically applies the same function consecutively over a set of values that gets modified, passing the result of the previous execution's set to the next one:

```js
const numeros = [1,2,3,4,5]
numeros.reduce((atual, acumulador) => acumulador + atual, 0)
// 1 => { atual: 1, acumulador: 0 }
// 2 => { atual: 2, acumulador: 1 }
// 3 => { atual: 3, acumulador: 3 }
// 4 => { atual: 4, acumulador: 6 }
// 5 => { atual: 5, acumulador: 10 }
// 6 => { atual: undefined, acumulador: 15 }
// 7 => resultado 15
```

This can also be done with what's called nesting, which is when we pass one function execution into another consecutively, so imagining we had the sum function we used in the `reduce` above, we could represent that same function like this:

```js
function soma (a, b) { return a + b }
soma(5, 
     soma(4, 
          soma(3, 
               soma(2, 
                    soma(1, 0)
                   )
              )
         )
    )
```

I think it's easy to see the problem here... Deep nesting, along with [currying](https://en.wikipedia.org/wiki/Currying), are techniques that, despite being fairly common in object-oriented languages too, are much more common in languages with more functional approaches like [Hack](https://en.wikipedia.org/wiki/Hack_\(programming_language\)), Clojure and [F#](https://en.wikipedia.org/wiki/F_Sharp_\(programming_language\)). That's because these languages, as the name suggests, are based on functions for working with data in a way that's a bit closer to the system known in mathematics as [Lambda Calculus](https://en.wikipedia.org/wiki/Lambda_calculus).

The point is that deep nesting is really hard to read, because we don't know where the initial data is coming from, and also because reading has to start from the inside out (or from right to left), since we need to know the result of the first function passed in order to infer the result of the last call.

On the other hand, deep nesting applies to pretty much every type of expression: we can have arithmetic operations, arrays, `await`, `yield`, and all sorts of things. For example, the function above could (and probably will, inside the [compiler](https://dev.to/khaosdoctor/node-js-por-baixo-dos-panos-10-otimizacoes-do-compilador-39oi)) be written like this:

```js
const resultado = (5 + 
 (4 + 
  (3 + 
   (2 + 
    (1 + 0)
   )
  )
 )
)
```

Currying is when we have functions that are unary by nature, so when we want to compose something, we return a function that's going to call another function. That way we can compose the two functions as if they were two calls, for example, a function that multiplies two numbers:

```js
const multiplicaDois = x => y => x * y
const resultado = multiplicaDois(5)(2) // -> 10
```

Currying, despite being elegant, is a bit costly because we have to type quite a bit more, and on top of that, longer and more complex functions end up getting harder for anyone to read. Still, currying is widely used, especially by libraries like [Ramda](https://ramdajs.com), which are built around currying from the ground up.

But there's another way of writing code that most of us are already a bit used to: **fluent interfaces**.

## Fluent Interfaces

You've probably run into fluent interfaces at some point in your life, even if you don't know what we're talking about. If you've ever used jQuery, or even the most common JavaScript array functions, you've already used a fluent interface.

This type of design is also called **method chaining**.

The big idea behind fluent interfaces is that you don't need to call the object again in order to run a different, but subsequent, function on the same data from your original object, for example:

```js
const somaDosImpares = [1, 2, 3]
	.map(x => x * 2)
	.filter(x => x % 2 !== 0)
	.reduce((prev, acc) => prev+acc, 0)
```

The biggest example so far of this architecture model is jQuery, which consists of a single main mega-object called `jQuery` (or `$`) that gets dozens and dozens of child methods, all of which return the same main object, so you can chain all of them together. This also looks a lot like a design pattern called **[builder](https://imasters.com.br/javascript/design-patterns-com-javascript-typescript-padroes-criacionais).**

Notice that I'm not calling my array again, I'm simply **chaining** (that's where the term "chaining" comes from) the methods of that array one after another, and I get the closest thing we have today to an interface that's both highly readable and also mimics the flow behavior we want to achieve with pipeline operators.

The problem is that this method's applicability is limited, because it's only possible if you're working within a paradigm that has functions designated as methods on a class, meaning when we're working directly with object orientation.

But on the other hand, when it's applied, reading and usability become so easy that a lot of libraries hack their code together just to be able to use method chaining. Think about it, when we have this type of design:

-   Our code flows from left to right, like we're used to
-   Every expression that could otherwise get nested stays at the same level
-   All the arguments are grouped under the same main element (the object in question)
-   Editing the code becomes trivial: if we need to add more steps, we just drop in a new function in the middle; if we need to remove one, we just delete the line

The biggest problem is that we can't fit every interface and function type into this same design, because we can't return arithmetic expressions (like `1+2`), or `await`, or `yield`, nor object literals or arrays. We're always going to be limited to what a function or method can do.

## Enter the pipe operators

Flow operators combine both worlds and improve the applicability of both models into a more unified and more readable interface. So instead of having a bunch of nested methods, or a bunch of functions, we can simply do this:

```js
const resultado = [1,2,3].map(x => x*2) |> %[0] // => 2
```

The syntax is simple: on the left side of the `|>` operator we have any expression that produces a value, and that produced value gets thrown into a placeholder (or temporary object) that, for now, is written as `%`, meaning `%` is the result of whatever is on the **left** of the `|>`. Then, on the right side of the operator, we have the transformation applied to the obtained result. The final result of these two expressions is the output, and that's what gets assigned to `resultado`.

If you check it out using [Babel](https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011%2C%20not%20ie_mob%2011&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=MYewdgzgLgBFICECGECmA2ALDAvDAFACYCUuAfDAgK4BmNqATgHQ0MgC2RxT8AylAwCWYAOb4A5ACMUGTOOIAoBaEixpaACqoAHrDziAFlCgAHCAC4A9JYA2EJGHgQmhVADdxMAD4UApDxAAVRMTRgBhGXxSHzhEGSx8X2IgA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env%2Cstage-0&prettier=false&targets=Node-16&version=7.17.8&externalPlugins=&assumptions=%7B%7D), for the code below:

```js
const toBase64 = (d) => Buffer.from(d).toString('base64')

const baseText = 'https://lsantos.dev' 
|> %.toUpperCase() 
|> toBase64(%)
```

We're going to get the following output:

```js
"use strict";

const toBase64 = d => Buffer.from(d).toString('base64');

const baseText = toBase64('https://lsantos.dev'.toUpperCase());
```

Likewise, if we use functions with currying, babel will be able to decipher that information and produce a valid representation.

Right now there are two most well-known implementations of pipe. The first one is F#'s, a functional programming language created by Microsoft based on OCaml. The second is Hack's, a language created by Facebook a good while back that is, essentially, PHP with static types.

The biggest difference between the operators is that, in Hack's version, it accepts any type of expression as a valid operand on both the left and right side of the expression, through the special variable `%`.

So we can do literally anything:

```js
value |> someFunction(1, %, 3) // function calls
value |> %.someMethod() // method call
value |> % + 1 // operator
value |> [%, 'b', 'c'] // Array literal
value |> {someProp: %} // object literal
value |> await % // awaiting a Promise
value |> (yield %) // yielding a generator value
```

In F#'s case, though, we're a bit more limited to unary functions, so the `%` variable doesn't exist. That means we always need some kind of function on the right side of the operator:

```js
const f = soma(1,2) |> x => soma(x, 3)
```

Among other reasons explained [here](https://2ality.com/2022/01/pipe-operator.html), the proposal is focusing mainly on being able to apply Hack's model to JavaScript, not F#'s.

## Conclusion

For now this operator is still trying to get off the ground, but there are already plans described [in this section](https://github.com/tc39/proposal-pipeline-operator#possible-future-extensions) showing that a few other options for extending the operator are already under consideration, like conditional and optional operators using `if` or `?`, and loop operators with `for of`, plus using this operator with `catch`.

There's still no date or timeline for this proposal to become reality, but plenty of eyes are on what's happening!
