Ikeh Akinyemi Ikeh Akinyemi is a software engineer based in Rivers State, Nigeria. He’s passionate about learning pure and applied mathematics concepts, open source, and software engineering.

How to create a compose function in TypeScript

5 min read 1641

How To Create Compose Function TypeScript

Composing functions in TypeScript is a core concept in functional programming that combines multiple functions into a single function that can perform any number of tasks you may require.

Function composition can be implemented in many programming languages, including TypeScript. In this article, we will learn how to create typed compose and pipe functions in TypeScript and how to use these functions to perform function composition in TypeScript.

Jump ahead:

TypeScript function composition

Function composition in TypeScript can be done by taking the output of one function and passing it as the input to another function.

This process can be repeated with multiple functions, forming a chain of functions that can be easily composed together to perform more complex tasks.

Function composition can be used to create more readable and maintainable code, as it allows you to define small, reusable functions that can be easily combined to perform larger tasks — this can help to reduce code duplication and make it easier to understand and debug your code.

There are two common approaches to function composition in functional programming: compose and pipe. The compose function combines functions from right to left, while the pipe function combines functions from left to right.

The choice of which approach to use depends on the specific needs of the task at hand. As we get into a use case, you should get an understanding of when to use either approach.

What are the compose and pipe functions?

The compose and pipe functions are higher-order functions that accept one or more functions as arguments and return a new function that combines the functionality of the input functions.

In other words, it pulls functions and makes things cleaner and easier to deal with.

compose function

The compose function combines functions from right to left, so the output of each function is passed as the input to the next function in the chain.

As an example, let’s take a look at the following functions:

function add(x: number): number {
  return x + 5;
}

function multiply(x: number): number {
  return x * 10;
}

function divide(x: number): number {
  return x / 2;
}

We can use the compose function to create a new function that first multiplies its arguments, then adds them together, and finally divides the result by two, as demonstrated here:

function compose<T, U, V, Y>(f: (x: T) => U, g: (y: Y) => T, h: (z: V) => Y): (x: V) => U {
  return (x: V) => f(g(h(x)));
}

const composedFunction = compose(
  divide,
  add,
  multiply
);

console.log(composedFunction(4)); // returns (4 * 10 + 5) / 2 = 22.5

The above definition for the compose function defines four possible type parameters that we used to define the three function arguments and the return type.



This definition can easily get confusing, but we’ll fix it in the next section using the Array.prototype.reduce method and generic type definition.

pipe function

The pipe function combines functions from left to right, so the output of each function is passed as the input to the previous function in the chain.

Using the same example functions as before, we can use the pipe function to create a new function that first divides its arguments, then adds them together, and finally multiplies the result by two. Take a look, here:

function pipe<T, U, V, Y>(f: (x: T) => U, g: (y: U) => V, h: (z: V) => Y): (x: T) => Y {
  return (x: T) =>  h(g(f(x)));
}

const pipedFunction = pipe(
  divide,
  add,
  multiply
);

console.log(pipedFunction(4)); // returns ((4 / 2) + 5) * 10 = 70

Using Array.prototype.reduce to create a compose function

We’ll use the Array.prototype.reduce method to create and chain custom functions into a compose function for this tutorial.

The Array.prototype.reduce() method is a higher-order function that applies a given function to each element of an array, resulting in a single output value. The function used to reduce the array elements is called the “reducer” function, and it takes in two arguments: the “accumulator”, and the current element being processed.

The accumulator is the result of the previous call to the reducer function; it is initialized with the first element of the array or with an optional initial value, if provided.

Now, let’s apply what we know from the above definition about Array.prototype.reduce() to create a compose function, as shown here:

const compose = <T>(fn1: (a: T) => T, ...fns: Array<(a: T) => T>) =>
  fns.reduce((prevFn, nextFn) => value => prevFn(nextFn(value)), fn1); 

This code defines a function called compose that takes in a list of functions (fn1, fns) and returns a new function that performs the composition of these functions.

The first function, fn1, is defined as a function that takes a value of type T and returns a value of T.

The rest of the functions, fns, are defined as an array of functions that also take in a value of type T and return a value of type T.


More great articles from LogRocket:


N.B., note the order we’re executing: the nextFn function comes before the prevFn function.

If we use this new definition against the previous example, it will produce the same result, but instead, let’s use the below example to see how the functions are chained together visually:

const func1 = (v: string) => `func1(${v})`;
const func2 = (v: string) => `func2(${v})`;
const func3 = (v: string) => `func3(${v})`;
const composedFunction = compose(func1, func2, func3);
console.log(composedFunction("value")); // func1(func2(func3(value))) 

Using Array.prototype.reduce to create a pipe function

The pipe function is similar to the Unix pipe operator, where the output of one command is passed as the input to the next.

We’ll also use the Array.prototype.reduce() to create a typed pipe function, but instead, we’ll flip the order in which the callback arguments are executed, as shown here:

const pipe = <T>(fn1: (a: T) => T, ...fns: Array<(a: T) => T>) =>
  fns.reduce((prevFn, nextFn) => value => nextFn(prevFn(value)), fn1); 

The above code is the same as the compose function, except we flipped the callback arguments.

We can test it using the previous example used for the compose function, which will give us the pipe version of the chained operation:

const pipedFunction = pipe(func1, func2, func3);
console.log(pipedFunction); // func3(func2(func1(value))) 

Now that we’ve demonstrated how to compose a function in TypeScript, let’s explore how to revamp the pipe function by extending its arguments before we conclude this article.

Extending the pipe function’s arguments

Currently, the pipedFunction defines only one argument, but there will be use cases where we need to process multiple pieces of input data and pass the aggregated output to the next function.

This raises the question of how we can design the pipe function to provide such support. Let’s find out; take a look below:

const pipe = <T extends any[], U>(
  fn1: (...args: T) => U,
  ...fns: Array<(a: U) => U>
) => {
  const piped = fns.reduce((prevFn, nextFn) => (value: U) => nextFn(prevFn(value)), value => value);
  return (...args: T) => piped(fn1(...args));
};

In the above snippet, we have redefined fn1 as a function that takes in a variable number of arguments of type T (which is a tuple of any type) and returns a value of type U. The fns parameter is still a rest parameter that represents an array of functions that take in a single argument of type U and return a value of type U.

The pipe function returns a new function that takes in a variable number of arguments of type T and returns a value of type U.

This new function executes all of the functions in the pipeline in sequence, starting with fn1 and ending with the last function in fns, while passing the output of each function as the input to the next function in the pipeline.

Let’s test the new definition by redefining the test example by supplying more than one argument to the first function:

const func1 = (v1: string, v2: string) => `func1(${v1}, ${v2}, ...)`;

const pipedFunction = pipe(func1, func2, func3);
console.log(pipedFunction); // func3(func2(func1(value1, value2, ...))) 

You may find yourself wondering about compose function in this regard; can we extend it too? Well, It is impossible to define the types for the compose function in the same way as the pipe function. This is because the types for pipe are determined by the type of the first function passed to it, while the types for compose are determined by the type of the last function.

However, defining the types for compose in this way would require using rest arguments at the beginning of the argument list, which is currently not supported.

Conclusion

To sum up, the compose and pipe functions are useful tools in functional programming for combining multiple functions into a single function or chain of functions.

These functions can help you create more readable, cleaner, and maintainable code by defining small, reusable functions and composing them to perform more complex tasks.

In this article, we learned how to create typed compose and pipe functions in TypeScript using generics and the built-in Array.prototype.reduce() method, and we also learned how to use these functions to perform function composition in TypeScript.

Whether you are new to functional programming or an experienced developer, understanding the concepts of compose and pipe functions can help you to write more efficient and effective code. Let me know about your experiences creating compose functions for your TypeScript projects or in general!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Ikeh Akinyemi Ikeh Akinyemi is a software engineer based in Rivers State, Nigeria. He’s passionate about learning pure and applied mathematics concepts, open source, and software engineering.

Leave a Reply