Building a Custom Pipe Function in TypeScript

2 april 2026

As TypeScript applications scale, data frequently passes through sequential transformations. While method chaining is prevalent in object-oriented paradigms, functional programming utilizes function composition. The pipe function serves as a core mechanism for this approach.

The Problem: Nested Functions

Without a pipe utility, applying multiple transformations typically requires nested function calls. This forces the code to be read from the inside out, which introduces unnecessary cognitive load and decreases maintainability.

const getPrice = (order: Order) => order.total;
const applyDiscount = (price: number) => price * 0.9;
const addTax = (price: number) => price * 1.25;

// Difficult to parse: execution order is inside-out.
const finalPrice = addTax(applyDiscount(getPrice(myOrder)));

The Solution: The Pipe Concept

A pipe function accepts an initial value and processes it through a sequence of provided functions. The output of the preceding function serves as the input for the subsequent one. This establishes a linear, predictable execution flow.

Full width image

The Logic

The core implementation in JavaScript relies on the rest parameter syntax and the array reduce method.

// Processes an initial value through an array of functions.
const pipeLogic = (initialValue, ...fns) => 
  fns.reduce((acc, fn) => fn(acc), initialValue);

The Challenge: Typing Pipe in TypeScript

Establishing strict TypeScript definitions for a pipe function requires careful handling, as each function in the sequence may return a distinct type. The standard architectural approach involves utilizing function overloads to define the type transformations sequentially.

The TypeScript Implementation

The following snippet illustrates a strictly typed pipe function supporting up to three composed functions. Additional overloads can be appended to support longer execution chains.

// 1. Function Overloads to define the strict input/output flow.
export function pipe<A, B>(value: A, fn1: (arg: A) => B): B;
export function pipe<A, B, C>(value: A, fn1: (arg: A) => B, fn2: (arg: B) => C): C;
export function pipe<A, B, C, D>(value: A, fn1: (arg: A) => B, fn2: (arg: B) => C, fn3: (arg: C) => D): D;

// 2. The implementation (typed with 'any' internally; the overloads govern the external API).
export function pipe(value: any, ...fns: Function[]): any {
  return fns.reduce((acc, fn) => fn(acc), value);
}

Application

Implementing the refactored pricing logic with the typed pipe function demonstrates its utility. TypeScript resolves the types continuously through the execution chain.

const myOrder = { total: 100 };

// Linear, top-to-bottom evaluation.
// A type mismatch between functions will trigger a compiler error.
const finalPrice = pipe(
  myOrder,
  getPrice,
  applyDiscount,
  addTax
);

console.log(finalPrice); // 112.5

Summary

Implementing a custom pipe function provides practical exposure to function composition and advanced TypeScript type resolution.

  • Readability: Code execution mirrors the written sequence.
  • Type Safety: Function overloads enforce type alignment between consecutive operations.
  • Modularity: The pattern necessitates the use of isolated, single-purpose functions.