Implementing the Pipe Operator in TypeScript
Tags: typescript, functional, programming, tutorial
Published: 2023-01-21 16:37:45

## What is the pipe operator?

<br />

The pipe operator is used to chain function calls together in a more readable and concise way. The operator takes the output of one function as the input for the next function in the chain. Here's an example of pipes in [Elixir](https://elixir-lang.org/).

```elixir
"hello"
|> String.upcase() # turn string into uppercase
|> String.reverse() # reverse the string returned from `String.upcase()`
|> IO.puts() # print the output from `String.reverse()`

# "OLLEH"
```

In the example above, the pipe operator `|>` takes the output of the previous function and passes it as the input for the next function. Here's a more complex example. It's my solution to [Advent of Code 2022](https://adventofcode.com/2022/) [day 1 pt.2](https://adventofcode.com/2022/day/1).

```elixir
File.read!("./input.txt")
|> String.split("\n\n")
|> Enum.map(fn elf ->
  elf
  |> String.split("\n")
  |> Enum.map(fn e -> e |> String.to_integer() end)
  |> Enum.sum()
end)
|> Enum.sort(:desc)
|> Enum.slice(0..2)
|> Enum.sum()
|> IO.inspect()
```

You can see how the pipe operator really helps in improving the readability of the code. The code reads like a sentence. Pipes also eliminate the need to create temporary variables to store the output of each function call or using nested function calls. As a result developers also don't have to worry about naming variables which is my least liked part of programming.

<br />

## Implementing the pipe operator in TypeScript

<br />

After watching [Theo's video on the proposal for adding pipes in JavaScript](https://youtu.be/h1FvtIJ6ecE), I was inspired to implement the pipe operator as a function that composes functions together. Here is what I set out to achieve.

```ts
const len = (s: string): number => s.length;
const double = (n: number): number => n * 2;
const square = (n: number): number => n ** 2;

console.log(pipe("hi", len, double, square)); // 16
```

### Initial implementation

<br />

This was my first iteration of the `pipe` function.

```ts
const pipe = (value: any, ...fns: Function[]) =>
  fns.reduce((acc, fn) => fn(acc), value);
```

Here's what this code is doing:

<br />

- The function takes two arguments: `value` of any type, and `fns` which is an array of functions. The spread operator `...` allows for any number of functions to be passed in.
- The [`reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) function is used to iterate over the array of functions and pass the output of the previous function as the input for the next function.
- The callback function passed to `reduce` takes two arguments: `acc` which is the accumulator, and `fn` which is the current function in the array. The first function in the array is applied to the initial value, which is the `value` argument passed to the function. Each subsequent function is applied to the output of the previous function, chaining them together.
- It then returns the output of the last function in the array.

<br />

And this works!

![First iteration of pipe function works!](<https://us-east-1.tixte.net/uploads/nexxel.needs.rest/Alacritty-v0.10.0-portable_(1)_csN4l17Yqf.png>)

### Problems with the initial implementation

<br />

There are a few problems with this implementation. The first problem that I immediately noticed is that the type of the returned value from the function is `any`.

![Type of returned value is any](<https://us-east-1.tixte.net/uploads/this-vegetable.is-from.space/Alacritty-v0.10.0-portable_(1)_bsSjmmSE39.png>)

Obviously this is not good. Ideally this type should be inferred by the TypeScript compiler. I got some help from Reddit and a user there suggested this:

```ts
type Fn = (...args: any[]) => any;

type LastReturnType<L extends Fn[]> = L extends [...any, infer Last extends Fn]
  ? ReturnType<Last>
  : never;

const pipe = <Funcs extends Fn[]>(value: any, ...fns: Funcs) =>
  fns.reduce((acc, fn) => fn(acc), value) as LastReturnType<Funcs>;
```

This is definitely some crazy TypeScript. Let's break it down.

<br />

- `Fn` is a type alias for a function that takes any number of arguments of `any` type and returns `any` type.
- `LastReturnType` is a generic type that takes an array of functions and returns the return type of the last function in the array. The `infer` keyword is used to infer the type of the last function in the array.
- It uses [variadic tuple types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types) to know what the last function in the tuple is and then uses the `ReturnType` type utility to get the return type of the last function.
- Then the `pipe` function is defined as a generic function that takes an initial value of any type and an arbitrary number of functions of the `Fn` type. It then casts the return value of the function to the return type of the last function in the array.

<br />

So now the type of the returned value is inferred correctly.

![Type of returned value is inferred correctly](<https://us-east-1.tixte.net/uploads/this-vegetable.is-from.space/Alacritty-v0.10.0-portable_(1)_T7Vnqd1kYN.png>)

But there's still a problem. Let me demonstrate it with an example.

```ts
// does not show an error
const result = pipe("hi", double, len, square);

// Argument of type 'number' is not assignable to parameter of type 'string'.
const result2 = square(len(double("hi")));
```

Here we're trying to double the string "hi", which is impossible as we're passing a string to a function that expects a number. You can see we get the appropriate error when we try to do this by nesting the function calls. But our `pipe` function does not show any errors.

![No error when passing a string to a function that expects a number](<https://us-east-1.tixte.net/uploads/this-vegetable.is-from.space/Alacritty-v0.10.0-portable_(1)_ugX7Y8OmQc.png>)

This is a perfect use case for [Higher Kinded Types](<https://en.m.wikipedia.org/wiki/Kind_(type_theory)>). Unfortunately, TypeScript does not support Higher Kinded Types yet. There isn't a way to say "for all these functions, the input type is [contravariant](<https://en.m.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)>) to the output type of the previous function".

<br />

### So what's the solution?

<br />

Turns out, the easiest and most straightforward solution is to set an upper bound on the number of functions that can be passed to the `pipe` function and use [function overloading](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads) to define the type of the returned value.

```ts
function pipe<A>(value: A): A;
function pipe<A, B>(value: A, fn1: (input: A) => B): B;
function pipe<A, B, C>(value: A, fn1: (input: A) => B, fn2: (input: B) => C): C;
function pipe<A, B, C, D>(
  value: A,
  fn1: (input: A) => B,
  fn2: (input: B) => C,
  fn3: (input: C) => D
): D;
function pipe<A, B, C, D, E>(
  value: A,
  fn1: (input: A) => B,
  fn2: (input: B) => C,
  fn3: (input: C) => D,
  fn4: (input: D) => E
): E;
// ... and so on

function pipe(value: any, ...fns: Function[]): unknown {
  return fns.reduce((acc, fn) => fn(acc), value);
}
```

This might seem very manual but it's definitely the best way to do this. It works very well and also gives fairly easy to understand type errors for the user. Let's see how it works.

<br />

The first five function declarations are overloads of the `pipe` function, each one of them has a different set of parameters, each overload corresponds to a different number of functions that can be passed to the `pipe` function.

<br />

- The first overload takes a single argument of generic type `A` and returns the same value without applying any function to it.
- The second overload takes two arguments, value of type `A`, and `fn1` a function that takes an argument of type `A` and returns a value of type `B`.
- The third overload takes three arguments, value of type `A`, `fn1` a function that takes an argument of type `A` and returns a value of type `B`, and `fn2` a function that takes an argument of type `B` and returns a value of type `C`.
- There can be any number of overloads, but I've only defined five for demonstration purposes.

<br />

Each overload corresponds to a different number of functions, and each function's input type is the output of the previous function, this way the `pipe` function will not only return the output type of the last function passed but also type check the input and output types of each function in the pipeline.

<br />

The last function declaration is the actual implementation of the `pipe` function. It takes an initial value of any type and an arbitrary number of functions and applies them to the initial value in the order they are passed. The logic is the same as before.

<br />

The advantage of this implementation is that it allows the `pipe` function to be more type-safe as it ensures that the input and output types of each function in the pipeline are consistent and match the type of the initial value, and it also ensures that the functions passed to the `pipe` function have the correct signature.

![Types working correctly](<https://us-east-1.tixte.net/uploads/nexxel.needs.rest/Alacritty-v0.10.0-portable_(1)_bn9nOVbkO0.png>)

<br />

### Overloading without using the function keyword

<br />

I love my arrow functions and almost never use the `fuction` keyword. Unfortunately, we can't use arrow functions for overloading. We can only use the `function` keyword for overloading. But what we can do is implement the overloads in an interface and then implement the actual function as a function of that interface.

```ts
interface Pipe {
  <A>(value: A): A;
  <A, B>(value: A, fn1: (input: A) => B): B;
  <A, B, C>(value: A, fn1: (input: A) => B, fn2: (input: B) => C): C;
  <A, B, C, D>(
    value: A,
    fn1: (input: A) => B,
    fn2: (input: B) => C,
    fn3: (input: C) => D
  ): D;
  <A, B, C, D, E>(
    value: A,
    fn1: (input: A) => B,
    fn2: (input: B) => C,
    fn3: (input: C) => D,
    fn4: (input: D) => E
  ): E;
  // ... and so on
}

const pipe: Pipe = (value: any, ...fns: Function[]): unknown => {
  return fns.reduce((acc, fn) => fn(acc), value);
};
```

This is same as the previous implementation, but we're using an interface to implement the overloads instead of using the `function` keyword. This way we can use arrow functions for the actual implementation. Pretty neat, right? Everything still works!

![Everything still works!](<https://us-east-1.tixte.net/uploads/this-vegetable.is-from.space/Alacritty-v0.10.0-portable_(1)_8WZ9RDzVlD.png>)

<br />

## Conclusion

<br />

I hope you enjoyed this article. I've learned a lot while writing it, and I hope you did too. Thanks for reading!

<br />

## References

<br />

- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [`LastReturnType` implementation](https://www.typescriptlang.org/play?#code/PTAECcFMBcFdwHamgTwA6VAewGagDYCGAztKDrAgMbQCWWStSh44hKAUKhqAGJOF8AJRjwEANUGxIAHl6hIAD2iQEAE2J9KNeggDaAXQB8AXj4LlqjaD0A6e0xyRwoAKoAaUI+egAMgdAAfj8LFXVNAAp7WxYAc2IALlBCBBQASlATIy8EJxchINACpIRIADcfEvLnDi50TABBTL4BYVFESXxpGT0IjKzQAEYAJgBmY1qqBlJQNFoeMzkETSUw615tOgZDIwiyqUgklJRPaJxlpN5ltMvWkTgOg6XibIGOUA-yZdsoNVgqSARCKEKhUTznfrZc7A0FpTz7LqQNIAbg+k2mZHwqmaEUSoFI4CYsRuoAQsAAtgAjHwDYi2LEIWLQAAWyI4U2WZDUWFglKxOIQJQp1PAJLJVJp2SQACpQMNUeyMfiAI6wFiYMwRQWk4XOMW6lwDGWy+WgdGciCQYiwfBkMxzDARABEzNoTvcDM83N5WM8xFV6pRoBAluttq8mmOHBDnwAeoFFcssFj6VhYhEoGHoBkQ4MAGwcIA) by u/i_fucking_hate_money
- [Remeda's `pipe` implementation](https://github.com/remeda/remeda/blob/master/src/pipe.ts)
- [u/GoodnessManifest's `pipe` implementation](https://github.com/bluesky-llc/open-source/blob/main/packages/utils/src/pipe.ts) with overloads in an interface
- [Proposal](https://github.com/tc39/proposal-pipeline-operator) for adding the pipe operator to JavaScript