Typescript reduce an array of function

644 views Asked by At

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.

1

There are 1 answers

2
captain-yossarian from Ukraine On

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.