How to use advanced Typescript to define a `pipe` function
Tags: typescript, programming, tutorial, functional
Published: 2022-06-11 17:25:49

Typescript is awesome but some functionnal libraries have limited implementation for Typescript definitions of :
- pipe
- compose
- flow

To remind you what these functions do, let take an example. Imagine you want to chain multiple functions calls :
```typescript
const n = -19;
const result = Math.floor(Math.sqrt(Math.abs(n))));
```
This can be hard to read hence the `pipe` function :
```typescript
const n = -19;
const result = pipe(n, Math.abs, Math.sqrt, Math.floor);
```
  
This is easier to read, because the read order is the same as the execution order. This becomes clearer as the number of composed functions grow.

### The problem

But unfortunately that's where most libraries have issues. Ideed, if you look closely at [fp-ts](https://github.com/gcanti/fp-ts/blob/master/src/function.ts) or [lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/lodash/fp.d.ts#L1319) pipe implementations you'll see that typescript support has a limited function count support.  
For fp-ts, after 19 composed functions, you'll have no more type checking.
For lodash, it's even worse, only 9 functions are supported.
This is because these libraries are using Typescript function overload definition instead of recursive Typescript definitions.  

So here we'll take a look at some advanced Typescript to define unlimited function parameters for `pipe` definition.

## Javascript pipe implementation

First we are implementing a version of the `pipe` function without type annotations :
```javascript
function pipe(arg, firstFn, ...fns) {
  return fns.reduce((acc, fn) => fn(acc), firstFn(arg));
}
```
It's pretty simple. An alternative `for` loop implementation would be :
```javascript
function pipe(arg, firstFn, ...fns) {
  let result = firstFn(arg);
  for (let fn of fns) {
    result = fn(result);
  }
  return result;
}
```
### How is pipe function constrained ?

When we say we want to add Typescript definition it's because if one of the functions result don't match the next function parameter type, we are likely to encounter a runtime error.
And it would be nice to catch this error at compile time, so the programmer knows early that there is a type missmatch.
  
To illustrate the constraints of the `pipe` function parameters, we can take the `fp-ts` implementation for 2 functions: 
```typescript
function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
``` 
what we can observe is that :
- The first function parameter type should match the first pipe parameter type
- The result type of intermediary functions should match the parameter type of the next function
- The result type of the last function is the result type of the pipe function

we'll now translate these contraint in Typescript.

### Typescript pipe definition

Let's add types with the final function definition. Don't be affraid, we'll explain everything :

```typescript
function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
  arg: Parameters<FirstFn>[0],
  firstFn: FirstFn,
  ...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
): LastFnReturnType<F, ReturnType<FirstFn>> {
  return (fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg));
}
```

This implementation is the same as the javascript one, but with some type annotations.

Let's decrypt everything :  
##### first line
```typescript
function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
```
This mean that pipe is a generic function with two generic parameters unknown when defining this function. But we know at least that all generic parameters should be functions. that's the meaning of `FirstFn extends AnyFunc` and `F extends AnyFunc[]`.

And `AsyncFn` is defined as :
```typescript
type AnyFunc = (...arg: any) => any;
```
  
#### first parameter  
```Typescript
  arg: Parameters<FirstFn>[0],
```
Typescript comes with some utility type. Here we are using [Parameters](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype) that extracts all parameters from the supplied function.
Here we only care about the first parameter of the first function passed as parameter, hence the `[0]` type array access.
This first `arg` is validating our first constraint :
- The first function parameter type should match the first pipe parameter type
  
#### second parameter
This one is pretty straightforward. It's the first function passed to the `pipe` one.

#### rest of the parameters
```typescript
  ...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
```
  
So here is the real deal. What is `PipeArgs` ? why not just telling Typescript that `fns` is of type `F`?  
This is where we are applying our second constraint :
- The result type of intermediary functions should match the parameter type of the next function

`PipeArgs` definition :
```typescript
type PipeArgs<F extends AnyFunc[], Acc extends AnyFunc[] = []> = F extends [
  (...args: infer A) => infer B
]
  ? [...Acc, (...args: A) => B]
  : F extends [(...args: infer A) => any, ...infer Tail]
  ? Tail extends [(arg: infer B) => any, ...any[]]
    ? PipeArgs<Tail, [...Acc, (...args: A) => B]>
    : Acc
  : Acc;
```

So `PipeArgs` is running through all function parameters and returning a new type with function definition where the return type of a function is the first parameter of the next function. It's a recursive type definition, and we are using Typescript `Tail` recursive optimization to be allowed to have around 1000 possible functions passed as `pipe` parameters.

For example, if we have this type definition (invalid `pipe` arguments since `D` is not of type `B` :
```typescript
type Input<A,B,C,D> = [(a: A) => D, (b: B) => C]
```
then we have in output :
```typescript
type Output<A,B,C,D> = PipeArgs<Input<A,B,C,D>>
// Output is [(a: A) => B, (b: B) => C]
```
The first function is now a valid `pipe` parameter, since we satisfy our second constraint.

Now all we have to do is check if this `PipeArgs<F>` is equal to `F`.  
  
If so, we have a valid definition. Else it's invalid. If it's invalid we return the valid definition so Typescript will point exactly where is the error.  
That's what this does :
```typescript
  ...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
```

#### result type constraint
```typescript
): LastFnReturnType<F, ReturnType<FirstFn>> {

```
And we are getting back the last function return type
and if it's not defined we use [ReturnType](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) utility type to return the first function result type.  
Here is the definition of `LastFnReturnType` by using [Typescript leading spread operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#leadingmiddle-rest-elements-in-tuple-types) to match the last function :
```typescript
type LastFnReturnType<F extends Array<AnyFunc>, Else = never> = F extends [
  ...any[],
  (...arg: any) => infer R
] ? R : Else;
```

### Conclusion

This is advanced Typescript for library authors. By adding this kind of type definition you can improve your type definitions for advanced functionnal code.  
I'd like to emphasize that Typescript type system is [Turing complete](https://github.com/microsoft/TypeScript/issues/14833), so if you think some advanced typing is impossible to do with Typescript, you should think twice.  
Because chances are that you can. It might not be easy and i hope Typescript will improve on this part.

For those that want to check the whole code, it's here on the [playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAggdiAYgVzgYygXigCgHQECGATgOYBcUhCAlFgHxUIDcAUK6JFADKEDOwRHABKEYMmJwAKuAgAeRFAgAPYBDgATPrGLFCIOfCSo09ADRQAogBs+0bHAgA3CMUbZFKtZu0BtVlBQBHjUIL4AumYBuMEkFEwgdJiMAJZwAGauUMKs4dEA-NnRlDZ2bByyUAAKKZAwZHwKSqrqWrAIKOgRFjBoGF6t2kadaBFYUBHuUJ4tPhPR+EQNlGmZxLBJqRlZAEK5BRPBvWgWiyHLGwxQO3mBlDPebb5ncXwr2+swmwkWwatZUkIKWstyghUBwOajz8ODi7zW12+oV+RAQEVBgUKNTqDTkEOsFl8Rz6p1iFy+Vxu9Gid1gfWKdLQ5XSJmAKQA9nAoGBavJECliAIhFDBu1jOgLA9RcMTJMcNE4dUSIQALZiVyNfmCwRwei+AAMkWi6QFQrg91NOqigWC6Tgb2qvPqpE1jAGc0UhUUlGxEGdrtYNEovDNonEkhkkAUFjDEmksgUlqE9EYAG9osQxHHcHbtPwxSMIjQ8JmNMg0BAcLCSVA7d87dW0DQLCbtUJYWQaDQ2ABfdhoTkCKBOQjWFIacY8yDywIAIgAjLPrbhCJQBMQ0qRvgA5ZAqgBGrlhzYWaEocD3h+I3wwAGooPPlzgNOfL65vgADAAkqY0PY-T4QGuwAbnAW5XLuB5HhANCBuUrADvawBQGkI5jhO2BTpW0QLkuCyrlA66bjub7EMeT5nlAF5QdeVyzneD54YEz6vjRn4-n+AELEBhEgcREGkTgMFwawQA)
  
If you this article was helpfull to you, don't forget to add a thumbs up.