Aplet's answer is great! But it gives up the type information about the function array when you pass a wrong argument. An error that just says Argument of type '(x: string) => number' is not assignable to parameter of type 'never'. when the error is actually originating from (x: string) => string isn't super helpful.
Unfortunately there doesn't really seem to be a robust method of doing this, but I'd still like to share my 2 cents-
type AnyFunc = (a: any) => any;
// Given a tuple of functions - deduce how much of it is flowable
type FlowFrom<T extends readonly AnyFunc[]> = T extends [(a: infer R) => any, ...AnyFunc[]]
? // Infer the argument type of the first function and pass it to `FlowFrom$`
FlowFrom$<R, T>
: // T was empty array
[];
// Actual function that deduces how much of a tuple is flowable, taking previous return type and remaining functions
type FlowFrom$<R, T extends readonly [(a: R) => any, ...AnyFunc[]]> = T extends [
(a: R) => infer R$,
...infer Tail
]
? Tail extends [(a: R$) => any, ...AnyFunc[]]
? // Valid, continue
[(a: R) => R$, ...FlowFrom$<R$, Tail>]
: // Tail has either been exhausted or invalid function has been found
Tail extends [(a: any) => any, ...AnyFunc[]]
? // Invalid function found, append a correct function type so it points out the specific error
// But make the appended function optional, the flow is still valid before this point
[(a: R) => R$, (a: R$) => any] | [(a: R) => R$]
: // Tail exhausted
[(a: R) => R$]
: // T is empty, exhausted tuple (impossible - apparently)
[];
FlowFrom deduces a "flowable" tuple type from a given tuple of functions. Essentially, it extracts all the functions in the tuple that will flow into each other, stopping whenever it encounters a function that is no longer flowable or when the tuple has exhausted.
// All the functions in the tuple type are flowable - the return type is the same
let valid: FlowFrom<[(x: string) => number, (x: number) => number]>;
// ^ [(a: string) => number, (a: number) => number]
// First function won't flow into the second one - stop at first one
let invalid: FlowFrom<[(x: string) => number, (x: string) => number]>;
// ^ [(a: string) => number, (a: number) => any] | [(a: string) => number]
This means, when you try to assign [(x: string) => parseInt(x, 10), (x: string) => x + '1'] to invalid, you get a nice error-
let invalid: FlowFrom<[(x: string) => number, (x: string) => number]> = [
(x: string) => parseInt(x, 10),
(x: string) => x + '1',
];
// ^ Type 'number' is not assignable to type 'string'
But, you can still assign the stuff before that breaking point-
let reducedButCorrect: FlowFrom<[(x: string) => number, (x: string) => number]> = [
(x: string) => parseInt(x, 10),
];
(in case you don't want this behavior, as in - being able to still assign everything before the breaking point when a breaking point is present, remove the union type from FlowFrom$, [(a: R) => R$, (a: R$) => any] | [(a: R) => R$] -> [(a: R) => R$, (a: R$) => any])
You can now use it like-
declare function flow<T extends readonly AnyFunc[]>(...args: T & FlowFrom<T>): unknown;
// Valid
flow(
(x: string) => parseInt(x, 10),
(x: number) => Boolean(x),
(x: boolean) => Number(x)
);
// Invalid
flow(
(x: string) => parseInt(x, 10),
(x: number) => Boolean(x),
(x: string) => Number(x)
);
// ^ Type 'boolean' is not assignable to type 'string'
Try all of this out on playground
Just wanted to share my two cents.