Note: Your functions always propagate input to the output, yet your types do not explicitly indicate this behavior. While generics could be used to convey this, it would complicate the solution provided below. As long as the function output is guaranteed to contain at least what was passed into it, the following solution will function correctly.
By utilizing a type parameter to deduce the array of functions as a tuple, and then employing a recursive conditional type to construct a tuple defining the expected types of the functions, you can intersect the type parameter with this validation type. This inspection serves as a check — if all is well, it essentially has no effect; otherwise, an error arises from the intersection:
type FunctionArray = Array<(p: any) => any>;
type ValidateChain<T extends FunctionArray , Input = {}, Result extends any[] = []> =
T extends [(data: Input) => infer R, ...infer Tail extends FunctionArray] ? ValidateChain<Tail, Input & R, [...Result, T[0]]>:
T extends [(...p: infer P) => infer R, ...infer Tail extends FunctionArray] ? ValidateChain<Tail, Input & R, [...Result, (data: Input) => R]>:
Result
type MergeAll<T extends FunctionArray , Input = {}> =
T extends [(data: any) => infer R, ...infer Tail extends FunctionArray] ? MergeAll<Tail, R & Input>: Input
const func = <T extends [] | FunctionArray>(modifiers: T & ValidateChain<T>): MergeAll<T> => {
return (modifiers as T).reduce((acc, modifier) => {
return modifier(acc)
}, {}) as MergeAll<T>
}
let r1 = func([addA]) // Pass
let r2 = func([addA, addB]) // Pass
let r3 = func([addA, addB, updateA]) //Pass.
let r4 = func([addA, addB, updateC]) // Fails
let r5 = func([updateA]) // Fails
Playground Link
To delve deeper into conditional types, refer to the handbook