Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typing a function with arbitrary number of distinct arguments

I have a function called pipeline() It's currently typed as so

function pipeline(firstFn: Function, ...fns: Function[]) {
  let previousFn = firstFn()
  for (const func of fns) {
    previousFn = func(previousFn)
  }
  return previousFn
}

If I use it to pipeline some functions

const add = (x: number) => (y: number) => x+y
const result = pipeline(
   () => 4,
   add(2),
   add(4)
)
// 10

However, with this solution there is not type-checking that the argument types of the functions match the return types of the previous functions, and the return type is simple any.

I realize this can't be completely typed (eg, Promise.all([]) has a max of 10 promises it can deal with before it gives up) but I'd like to have type-checking on invocations of at least a few arguments.

like image 701
reconbot Avatar asked Dec 03 '25 04:12

reconbot


1 Answers

A Simple Solution

In the case where the return type of each function in the pipeline is the same, this can be achieved with generics fairly easily:

function pipeline<T>(firstFn: () => T, ...fns: ((arg: T) => T)[]): T {
    let previousFn = firstFn()
    for (const func of fns) {
      previousFn = func(previousFn)
    }

    return previousFn
}

A more complete solution

The first thing we need to understand is how does Promise.all to it?

From the typescript source lib.es2015.promise.d.ts:

all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;

all<T1, T2, T3, T4, T5, T6, T7, T8, T9>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>;

all<T1, T2, T3, T4, T5, T6, T7, T8>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>;

...

all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;

They've written an overload for the function for every number of elements starting with 10 and going all the way down to the generic catch-all.

This is why Promise.all strict typing on the returned array bails out at more than 10 differently typed elements.

If you want to replicate this behaviour, you'll need to go to the same effort. Here's what this would look like for up to three parameters:

interface PipelineInterface {
    <T0, T1, T2>(a0: () => T0, a1: (a: T0) => T1, a2: (a: T1) => T2): T2;
    <T0, T1>(a0: () => T0, a1: (a: T0) => T1): T1;
    <T>(firstFn: () => T, ...fns: ((arg: T) => T)[]): T;
}

const pipeline: PipelineInterface = function (firstFn, ...fns) {
    let previousFn = firstFn()
    for (const func of fns) {
      previousFn = func(previousFn)
    }

    return previousFn
}

const result = pipeline(
    () => 4,
    (a) => a + 'Hello'
)
// result has type string

const result2 = pipeline(
    () => 4,
    (a: string) => a + 'Hello'
)
// Argument of type '(a: string) => string' is not assignable to parameter of type '(a: number) => string'.
like image 92
rh16 Avatar answered Dec 09 '25 05:12

rh16



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!