Typescript reduce an array of function
Asked Answered
H

1

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.

Horizontal answered 29/11, 2020 at 5:8 Comment(3)
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.Antilogarithm
I ended up writing a bunch of overloads and the general case of <S, R, Fns extends readonly ((source: any) => 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.Ekaterina
I doubt this will ever be possible with TypeScript. fp-ts' version of pipe also uses overloads: github.com/gcanti/fp-ts/blob/master/src/function.ts#L315Silverplate
L
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://mcmap.net/q/436715/-typescript-check-for-the-39-any-39-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.

Lukasz answered 30/5, 2021 at 10:38 Comment(2)
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.Kedge
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...Kedge

© 2022 - 2024 — McMap. All rights reserved.