Check out this robust example of a strongly-typed compose function in TypeScript. While it may have the limitation of not verifying each individual function type along the way, it excels at determining the argument and return types for the final composed function.
Strongly-Typed Compose Function
/** Definition for single argument function */
type Function<Arg, Return> = (arg: Arg) => Return;
/**
* Combines 1 to n functions.
* @param func initial function
* @param funcs additional functions
*/
export function compose<
F1 extends Function<any, any>,
FN extends Array<Function<any, any>>,
R extends
FN extends [] ? F1 :
FN extends [Function<infer A, any>] ? (a: A) => ReturnType<F1> :
FN extends [any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
FN extends [any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
FN extends [any, any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
FN extends [any, any, any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
Function<any, ReturnType<F1>> // Unlikely scenario to pipe so many functions, but we can still infer the return type if needed
>(func: F1, ...funcs: FN): R {
const allFuncs = [func, ...funcs];
return function combined(raw: any) {
return allFuncs.reduceRight((memo, func) => func(memo), raw);
} as R
}
Example Implementation:
// The compiler can recognize that the input type is Date from the last function
// and the return type is string from the first
const composition: Function<Date, string> = compose(
(a: number) => String(a),
(a: string) => a.length,
(a: Date) => String(a)
);
const outcome: string = composition(new Date());
Technical Workings:
We apply reduceRight on an array of functions to pass input through each function starting from the last one to the first. To determine the return type of compose, we infer the argument type based on the last function's argument type and the final return type based on the first function's return type.
Strongly-Typed Pipe Function
In addition to the compose function, we can create a strongly-typed pipe function to sequentially process data through each function.
/**
* Establishes a sequence of functions.
* @param func initial function
* @param funcs additional functions
*/
export function pipe<
F1 extends Function<any, any>,
FN extends Array<Function<any, any>>,
R extends
FN extends [] ? F1 :
F1 extends Function<infer A1, any> ?
FN extends [any] ? Function<A1, ReturnType<FN[0]>> :
FN extends [any, any] ? Function<A1, ReturnType<FN[1]>> :
FN extends [any, any, any] ? Function<A1, ReturnType<FN[2]>> :
FN extends [any, any, any, any] ? Function<A1, ReturnType<FN[3]>> :
FN extends [any, any, any, any, any] ? Function<A1, ReturnType<FN[4]>> :
Function<A1, any> // Unlikely scenario to pipe so many functions, but we can infer the argument type even when the return type is uncertain
: never
>(func: F1, ...funcs: FN): R {
const allFuncs = [func, ...funcs];
return function processed(raw: any) {
return allFuncs.reduce((memo, func) => func(memo), raw);
} as R
}
Usage Example:
// The compiler infers the argument type as number based on the first function's argument type and
// deduces the return type from the last function's return type
const piping: Function<number, string> = pipe(
(a: number) => String(a),
(a: string) => Number('1' + a),
(a: number) => String(a)
);
const output: string = piping(4); // results in '14'