1

Say I have an array of function where each function accepts the return value of the previous function and I call Array#reduce on that function with an initial value which the first function in the array accepts. This is perfectly sound and I would expect the return type to be the return type of the last function.

However TypeScript will not allow me to do this (see playground).

More pragmatically, I’m trying to write a generic pipe function which will compose the functions given as the ...rest and “pipe” the first argument into the composed function:

function pipe(source, ...fns) {
  return fns.reduce((value, fn) => fn(value), source);
}

And I simply cannot find a way to type this, even with varadic tuple types.

Even if I try to write out the function recursively I’m not really sure how to type it:

function pipe<
  S,
  R,
  Fns extends readonly unknown[],
>(source: S, ...fns: [(source: S) => R, ...Fns]): R {
  if (fns.length === 0) {
    return source;
  }

  const [fn, ...rest] = fns;

  return pipe(fn(source), rest);
}

See playground.

Rúnar Berg
  • 4,229
  • 1
  • 22
  • 38
  • 1
    I was curious if you'd get an answer. Give variadic functions a proper type is hard in general. Even in Haskell you need some lang extensions to do it. However, TS does neither excel in paryametric polymorphism nor in bounded paryametric polymorphism, both of which are required in FP. –  Dec 01 '20 at 11:46
  • I ended up writing a bunch of overloads and the general case of ` any)[]>(source: S, ...fns: [(source: S) => any, ...Fns, (source: any) => R]) => R`. That is `fns` is a variadic tuple that starts with `S => any` and ends with `any => R`. I also had to `// @ts-ignore` the return value. – Rúnar Berg Dec 01 '20 at 19:50
  • 2
    I doubt this will ever be possible with TypeScript. _fp-ts_' version of `pipe` also uses overloads: https://github.com/gcanti/fp-ts/blob/master/src/function.ts#L315 – chautelly Dec 02 '20 at 03:00

1 Answers1

3

Does it work for you ?

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;
// credits goes to https://stackoverflow.com/questions/55541275/typescript-check-for-the-any-type
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
type IsAny<T> = IfAny<T, true, never>;

type HandleAny<T extends Fn, U> =
    IsAny<Head<Parameters<T>>> extends true ?
    (a: U) => ReturnType<T>
    : T

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
    ? Head<Parameters<Fst>> extends ReturnType<Head<Lst>>
    ? Allowed<Lst, [...Cache, HandleAny<Fst, ReturnType<Head<Lst>>>]>
    : never
    : never
    : never
    : never
    : never;

type LastParameterOf<T extends Fn[]> =
    Last<T> extends Fn
    ? Head<Parameters<Last<T>>>
    : never

type Return<T extends Fn[]> =
    Head<T> extends Fn
    ? ReturnType<Head<T>>
    : never


function compose<T extends Fn, Fns extends T[], Allow extends {
    0: [never],
    1: [LastParameterOf<Fns>]
}[Allowed<Fns> extends never ? 0 : 1]>
    (...args: [...Fns] & Allowed<Fns>): (...data: Allow) => Return<Fns>

function compose<
    T extends Fn,
    Fns extends T[], Allow extends unknown[]
>(...args: [...Fns]) {
    return (...data: Allow) =>
        args.reduceRight((acc, elem) => elem(acc), data)
}

const foo = (arg: 1 | 2) => [1, 2, 3]
const bar = (arg: string) => arg.length > 10 ? 1 : 2
const baz = (arg: number[]) => 'hello'

/**
 * Ok, but you need explicitly add allowed type
 */
const check = compose((a: string) => a, baz)([1, 2, 3]) // [number]

/**
 * Errors
 */
// error because no type
const check_ = compose((a) => a, baz)([1, 2, 3])
// error because `a` expected to be string instead of number
const check__ = compose((a: number) => a, baz)([1, 2, 3])

Playground

Here, in my blog, you can find an explanation. Let me know if you are still interested in this question, I will try to provide more examplations or examples.

  • 1
    This work, but replacing for example the `bar` function in the composition by an anonymous show that the arg type is not inferred. Do you have a solution for this ? It is the very last thing I'm struggling with to implement a variadic pipe. – Bonlou Jul 21 '21 at 16:44
  • I definitly need to read your blog post to understand you Allowed type, it's very powerfull, it would have been perfect without the cast to never when arg and return type mismatch between step in the compose. There should be a way, but regarding the effort, reasonable classical overloads seems to still be the way to go for now... – Bonlou Jul 21 '21 at 21:57