Seems, Anders comment is obsolete.
type Foo = typeof foo
type Bar = typeof bar
type Baz = typeof baz
type Fn = (a: any) => any
type Head<T extends any[]> = T extends [infer H, ...infer _] ? H : never
type Last<T extends any[]> = T extends [infer _]
? never
: T extends [...infer _, infer Tl]
? Tl
: never
type Allowed<T extends Fn[], Cache extends Fn[] = []> = T extends []
? Cache
: T extends [infer Lst]
? Lst extends Fn
? Allowed<[], [...Cache, Lst]>
: never
: T extends [infer Fst, ...infer Lst]
? Fst extends Fn
? Lst extends Fn[]
? Head<Lst> extends Fn
? ReturnType<Fst> extends Head<Parameters<Head<Lst>>>
? Allowed<Lst, [...Cache, Fst]>
: never
: never
: never
: never
: never
type FirstParameterOf<T extends Fn[]> = Head<T> extends Fn
? Head<Parameters<Head<T>>>
: never
type Return<T extends Fn[]> = Last<T> extends Fn ? ReturnType<Last<T>> : never
function pipe<
T extends Fn,
Fns extends T[],
Allow extends {
0: [never]
1: [FirstParameterOf<Fns>]
}[Allowed<Fns> extends never ? 0 : 1]
>(...args: [...Fns]): (...data: Allow) => Return<Fns>
function pipe<T extends Fn, Fns extends T[], Allow extends unknown[]>(
...args: [...Fns]
) {
return (...data: Allow) => args.reduce((acc, elem) => elem(acc), data)
}
const foo = (arg: string) => [1, 2, 3]
const baz = (arg: number[]) => 42
const bar = (arg: number) => ['str']
const check = pipe(foo, baz, bar)('hello') // string[]
const check3 = pipe(baz, bar)([2]) // string[]
const check2 = pipe(baz, bar)('hello') // expected error
There is also a nice fnts library which uses Compose
type with better error handling