Pipelines in TypeScript with Generics
Tags: typescript, beginners, generics
Published: 2021-01-13 21:04:42

I first encountered a "Pipeline" in code when working with Laravel a few jobs ago. Not long afterwards, I started learning Elixir in my spare time, which had a similar concept with its [Pipe Operator]. And of course, if you're familiar with Bash you'll know about piping output into other functions using the pipe `|`.

[Pipe Operator]: https://elixirschool.com/en/lessons/basics/pipe-operator/

At the time, I really liked the expressive API that Laravel had created, allowing you to easily take an initial value, perform discrete operations on it and then spit it out or do something with it at the end.

I'd forgotten all about this feature, up until a few weeks ago when I wanted something similar in TypeScript. For example:

```typescript
pipe(5)
  .throughFunctions(
    n => n + 2,
    n => n * 2
  )
  .return(); // 14
```

Of course, this example is just with a simple `number` and some predefined functions. But, imagine you were dynamically gathering a list of operations based on something:

```typescript
const operations: ((arg0: number) => number)[] =
  gatherOperations();

pipe(5).throughFunctions(...operations).return()
```

Or maybe you're writing a small web framework, and wanted to pipe a request object through some middleware:

```typescript
const IsAdminMiddleware = (request: Req) => {
    if (!request.body.isAdmin) {
        throw new AuthorizationError("Request is not from an Admin")
    }

    return request;
}

pipe(request)
  .throughFunctions(IsAdminMiddleware)
  .then((request) => {
    console.log(`Request ${request.id} processed successfully`)
  });
```

Hopefully you can see how this pattern might be useful to implement in your own code! Let's dig in and create a basic Pipeline.

---

_**Note:** The Pipeline class we are creating will be limited to accepting and returning values of the same type. For example, once a pipeline is initialised with a `number`, only functions that accept and return a `number` can be used. Of course, there's nothing stopping you from expanding on this code to allow the type of the piped value to change as it progresses through the pipeline, but it's out of scope for this tutorial._

## Creating the Pipeline Class

Under the hood, the `pipe` method we used above just creates a new instance of a `Pipeline` class. Let's create this. We're using generics here to enable some type guarantees when passing functions to our pipeline.

```typescript
class Pipeline<T> {
  constructor(private param: T) {}
}

new Pipeline(5)

new Pipeline<string>("Hello")
```

And then let's create the shorthand `pipe` function:

```typescript
function pipe<T>(value: T): Pipeline<T> {
  return new Pipeline<T>(value);
}
```

## Creating the `throughFunctions` method

We want to be able to pipe this data through a list of functions, but we also want type safety around these functions, so that we can catch more errors when we are developing.

For example, imagine if we _didn't_ have type safety:

```typescript
pipe("hello").throughFunctions(
  // What happens when you multiply "hello" by 2?
  // You get a NaN, that's what.
  n => n * 2
).return();
```

So, we want our `throughFunctions` method to use the type that we defined when creating our Pipeline class:

```typescript
class Pipeline<T> {
    // ...
    
    throughFunctions(...funcs: ((param: T) => T)[]): Pipeline<T> {
        funcs.forEach(func => {
            this.param = func(this.param);
        });

        return this;
    }
}
```

Unpacking this, you'll see this is a _variadic_ method, meaning we can pass in one or more of the value for `funcs`. The type of the funcs parameter is `(param: T) => T)[]` — this means an array (`[]`) of functions that have one parameter of type `T` that return the type `T`. For example, our `n => n + 2` function in the introduction.

You should also note that the function returns the type `Pipeline<T>` — this is the type of the current class! This allows us to chain together multiple methods, which you'll often find is called a _fluent interface_ or _method chaining_.

Inside the function is pretty straight forward. We're looping through each of the functions that have been passed in and are executing them, passing in the current value and reassigning it to the result.

At the end of the `throughFunctions` method, we're returning `this` to satisfy the `Pipeline<T>` return value.

## Creating a `return` method

Phew! Almost done. Our Pipeline can now pass our data through as many functions as it wishes. Now all we need is a way to unpack the value contained inside it.

Let's create a `return` method on our class. (I wasn't sure if creating a method called "return" was valid, but it seems to work without issue!)

```typescript
class Pipeline<T> {
    // ...

    return(): T {
        return this.param;
    }
}
```

That's it! We're saying that the return type will be `T`, which will match the type of the initial value we passed in. Then, we just return the private property `param`, which we defined back in the Pipeline class's `constructor`.

---

# Conclusion

That should just about do it!

As I mentioned earlier, this is a fairly basic implementation of the Pipeline pattern, but has suited my needs so far. If you're tempted to push this further, why not try:

- Allowing objects to be passed in via a `throughObjects` method. These objects would need to implement an interface (maybe have a `handle` method: `throughObjects(...objs: { handle(arg0: T): T }[]): Pipeline<T>`) to ensure that they could be called successfully.
- Allowing the type to be mutated as the Pipeline progressed. This would complicate the Pipeline a little, but would make it way more powerful. For example, starting with a number, and allowing further steps to turn this into an array of numbers, then an object that contains that array.

---

Photo by [Quinten de Graaf] on Unsplash

[Quinten de Graaf]: https://unsplash.com/@quinten149