compose
The compose utility performs functional composition from right to left. It takes multiple functions and returns a single function that passes its result from one call to the previous one, following standard mathematical notation $f(g(x))$.
Implementation
View Source Code
ts
/** biome-ignore-all lint/suspicious/noExplicitAny: - */
import { isPromise } from '../typed/isPromise';
import type { FnDynamic } from '../types';
import { assert } from './assert';
type LastParameters<T> = T extends [...any, infer Last extends FnDynamic] ? Parameters<Last> : never;
type FirstReturnType<F> = F extends [infer First extends FnDynamic, ...any] ? ReturnType<First> : never;
type ComposeReturn<T extends FnDynamic[]> = (
...args: LastParameters<T>
) => FirstReturnType<T> extends Promise<any> ? Promise<Awaited<FirstReturnType<T>>> : FirstReturnType<T>;
/**
* Composes multiple functions into a single function. It starts from the rightmost function and proceeds to the left.
*
* @example
* ```ts
* const add = (x) => x + 2;
* const multiply = (x) => x * 3;
* const subtract = (x) => x - 4;
* const composedFn = compose(subtract, multiply, add);
* composedFn(5); // ((5 + 2) * 3) - 4 = 17
* ```
*
* @example
* ```ts
* const square = async (x) => x * x;
* const add = async (x) => x + 2;
* const composedFn = compose(square, add);
* await composedFn(4); // (4 * 4) + 2 = 18
* ```
*
* @param fns - List of the functions to be composed.
*
* @returns A new function that is the composition of the input functions.
*/
export function compose<T extends FnDynamic[]>(...fns: T): ComposeReturn<T> {
assert(fns.length > 0, 'compose requires at least one function', { args: { fns } });
const lastFn = fns[fns.length - 1];
const restFns = fns.slice(0, -1);
return ((...args: LastParameters<T>) =>
restFns.reduceRight(
(prev, fn) => (isPromise(prev) ? prev.then(fn) : fn(prev)),
lastFn(...args),
)) as ComposeReturn<T>;
}Features
- Isomorphic: Works in both Browser and Node.js.
- Async Support: Automatically handles Promises. If any function in the chain returns a Promise, the final result will be a Promise.
- Type-safe: Properly infers input and output types through the entire chain.
- Right-to-Left: Executes functions in reverse order of provided arguments.
API
ts
function compose<T extends any[], R>(
...fns: [(arg: any) => R, ...Array<(arg: any) => any>, (...args: T) => any]
): (...args: T) => R | Promise<R>;Parameters
...fns: A sequence of functions to be composed.
Returns
- A new function that represents the composition.
Examples
Synchronous Composition
ts
import { compose } from '@vielzeug/toolkit';
const addTwo = (n: number) => n + 2;
const double = (n: number) => n * 2;
// result = addTwo(double(x))
const calculate = compose(addTwo, double);
calculate(5); // (5 * 2) + 2 = 12Asynchronous Composition
ts
import { compose, delay } from '@vielzeug/toolkit';
const saveToDb = async (data: string) => {
await delay(10);
return { success: true, data };
};
const format = (s: string) => s.trim().toUpperCase();
const processAndSave = compose(saveToDb, format);
await processAndSave(' hello '); // { success: true, data: 'HELLO' }Implementation Notes
- If only one function is provided, it is returned as-is.
- Uses
reduceRightinternally to chain function calls. - Throws
TypeErrorif any provided argument is not a function.