Introducing The Recursive `Pipe` and `Compose` Types
Tags: discuss, typescript, challenge, showdev
Published: 2019-04-12 01:05:26

It turns out, the recursive `Pipe` (and `Compose`) types offer key advantages over the traditional method of using parameter overloading. The key advantages are:

* Preserve variable names
* Better tolerance of generics in function signature
* Variadic entry function
* Can theoretically compose unlimited functions

![](https://thepracticaldev.s3.amazonaws.com/i/vifs20qhv6dc1o5vgknh.png)

![](https://thepracticaldev.s3.amazonaws.com/i/8dtdgyhzd5i8dc4x2tw8.png)

In this article, we'll explore how such a `Pipe` and `Compose` type are built.

If you want to follow along with the full source at hand, see this repo:

https://github.com/babakness/pipe-and-compose-types

# Introduction

My journey for this project starts as a challenge to create recursive `Pipe` and `Compose` types, without relying on overloads that rename the parameters. To review an example of how to build this with overloads follow one of these links to the excellent library `fp-ts` by @gcanti

Pipe:

https://github.com/gcanti/fp-ts/blob/master/src/function.ts#L222

Compose:

https://github.com/gcanti/fp-ts/blob/master/src/function.ts#L160

I've used this strategy in my own projects. Parameter names are lost and are replaced, in this case, with alphabetic names like `a` and `b`.

TypeScript shares its ecosystem with JavaScript. Because JavaScript lacks types, parameter names can be especially helpful in guiding usage.

Let's look at how these types work a bit closer.

# Make a Recursive Pipe Type

First, we're going to need some helper types. These are going to extract information from a generic function:

```typescript
export type ExtractFunctionArguments<Fn> = Fn extends  ( ...args: infer P ) => any  ? P : never
export type ExtractFunctionReturnValue<Fn> = Fn extends  ( ...args: any[] ) => infer P  ? P : never
```

Next two more helpers, a simple type to allow us to branch different types predicated on the test type and a short hand for express any function.

```ts
type BooleanSwitch<Test, T = true, F = false> = Test extends true ? T : F
export type AnyFunction = ( ...args: any[] ) => any
```

This next type is really esoteric and ad-hoc:

```typescript

type Arbitrary = 'It was 1554792354 seconds since Jan 01, 1970 when I wrote this' 
type IsAny<O, T = true, F = false> = Arbitrary extends O
  ? any extends O
    ? T
    : F
  : F
```

Essentially, this type detects `any` and `unknown`. It gets confused on `{}`. At any rate, it isn't exported and intended for internal use.

With those helpers in place, here is the type Pipe:

```typescript
type Pipe<Fns extends any[], IsPipe = true, PreviousFunction = void, InitalParams extends any[] = any[], ReturnType = any> = {
  'next': ( ( ..._: Fns ) => any ) extends ( ( _: infer First, ..._1: infer Next ) => any )
    ? PreviousFunction extends void
        ? Pipe<Next, IsPipe, First, ExtractFunctionArguments<First>, ExtractFunctionReturnValue<First> >
        : ReturnType extends ExtractFunctionArguments<First>[0]
          ? Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
          : IsAny<ReturnType> extends true
            ? Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
            : {
              ERROR: ['Return type ', ReturnType , 'does comply with the input of', ExtractFunctionArguments<First>[0]],
              POSITION: ['Position of problem for input arguments is at', Fns['length'], 'from the', BooleanSwitch<IsPipe, 'end', 'beginning'> , 'and the output of function to the ', BooleanSwitch<IsPipe, 'left', 'right'>],
            }
    : never
  'done': ( ...args: InitalParams ) => ReturnType,
}[
  Fns extends []
    ? 'done'
    : 'next'
]
```

<img src="https://media.giphy.com/media/vmv47p4zksWDC/giphy.gif">


This type goes through a series of steps, it starts by iterating through each function, starting at the head and recursively passing the tail end to the next iteration. The key to making this work is to extract and separate first item in the array of functions from the rest using this technique:

```ts
( ( ..._: Fns ) => any ) extends ( ( _: infer First, ..._1: infer Next ) => any )
```

If we didn't do error checks, we could distill this next part as simply

```ts
PreviousFunction extends void
        ? Pipe<Next, IsPipe, First, ExtractFunctionArguments<First>, ExtractFunctionReturnValue<First> >
        : Pipe<Next, IsPipe, First, InitalParams, ExtractFunctionReturnValue<First> >
```

`PreviousFunction` is void only on the first iteration. In that case we extract the initial parameters. We pass `InitialParams` back in each iteration with the last functions return type. Once we exhaust all functions in the list, this part

```ts
 Fns extends []
    ? 'done'
    : 'next'
```

returns `done` and we can return a new function made up of the initial parameters and the last return type

```ts
'done': ( ...args: InitalParams ) => ReturnType,
```

The other bits are error detection. If it detects an error, it will return custom object which will point to the count were the error occurred. In other words, it has built-in error reporting.

I learned about this technique studying other people's libraries. One notable example is `typescript-tuple` which we use later to construct `Compose`

Alright now let's create an alias for the pipe function itself

```typescript
type PipeFn = <Fns extends [AnyFunction, ...AnyFunction[]] >( 
  ...fns: Fns & 
    Pipe<Fns> extends AnyFunction 
      ? Fns 
      : never 
) =>  Pipe<Fns>
```

Here is another technique to illustrate. When our `Pipe` function return that helpful error object, we want to actually raise a compiler error too. We do this by joining the matched type for `fns` conditionally to either itself or `never`. The later condition creating the error.

Finally, we're ready to define pipe. 

I do this in a different project, not only in a different file in the same project. I do this for two reasons:

First, I want to separate the implementation from the type. You're free to use these types without potentially including any JavaScript. 

Second, once the type has compiled correctly, I want to separate the pros and cons of future TypeScript versions from the type and implementation.

#  Implementing The Pipe Function


```ts
export const pipe: PipeFn =  ( entry: AnyFunction, ...funcs: Function1[] ) =>  ( 
  ( ...arg: unknown[] ) => funcs.reduce( 
    ( acc, item ) => item.call( item, acc ), entry( ...arg ) 
  ) 
) 
```

Let see it work: 

```ts
const average = pipe(
  ( xs: number[]) => ( [sum(xs), xs.length] ),
  ( [ total, length ] ) => total / length
)

```

✅ We see average has the right type `(xs: number[]) => string ` and parameter names are preserved.


Let's try another example:

```ts
const intersparse = pipe( 
  ( text: string, value: string ): [string[], string] => ([ text.split(''), value ]),
  ( [chars, value]: [ string[], string ] ) => chars.join( value )
)
```

✅ Both parameter names are preserved `(text: string, value: string) => string`

Let's try a variadic example:

```ts
const longerWord = ( word1: string, word2: string ) => (
  word1.length > word2.length 
    ? word1 
    : word2
)
const longestWord = ( word: string, ...words: string[]) => (
  [word,...words].reduce( longerWord, '' )
)

const length = ( xs: string | unknown[] ) => xs.length

const longestWordLength = pipe(
  longestWord,
  length,
)
```
✅ Parameter names and types check, the type for `longestNameLength` is ` (word: string, ...words: string[]) => number`


Great! 

# Compose

It turns out we can do this for `Compose` very easily. The helper we need we'll use from `typescript-tuple`.

```ts
import { Reverse } from 'typescript-tuple'
export type Compose<Fns extends any[]> = Pipe<Reverse<Fns>, false>
export type ComposeFn = <Fns extends [AnyFunction, ...AnyFunction[]] >( 
  ...fns: Fns & 
    Compose<Fns> extends AnyFunction 
      ? Fns 
      : never 
) =>  Compose<Fns>

```

The implementation is only slightly different

```ts
import { ComposeFn } from 'pipe-and-compose-types'
export const compose: ComposeFn = ( first: Function1, ...funcs:  AnyFunction[] ): any => {
  /* `any` is used as return type because on compile error we present an object, 
      which will not match this */
  return ( ...arg: unknown[] ) => init( [first, ...funcs] ).reduceRight( 
    (acc, item) => item.call( item, acc ), last(funcs)( ...arg ) 
  )
}
```

Let's put our new `compose` to the test:

```ts
const longestWordComposeEdition = compose(
  length,
  longestWord,
)
```

✅ Parameter names and types check, the type for `longestNameLength` is ` (word: string, ...words: string[]) => number`

# Closing

I encourage you to take a look at this repo to review the types

https://github.com/babakness/pipe-and-compose-types

to import the types into your own project, install using: 

`npm install pipe-and-compose-types`

Also look at two great application of these types

https://github.com/babakness/pipe-and-compose

import these functions into your project using

`npm install pipe-and-compose`

Please share your thought! Feel free to reach out to me on <a href="https://www.twitter.com/babakness">Twitter</a> as well!