1

I'm trying to write a function that works in a similar way to Promise.all but instead of all the promises executing in parallel I'd like to pass an array of callbacks and execute them sequentially. This is the JS code I've written:

function promiseAllSequential(promises) {
  return promises.reduce(
    (promiseAccumulator, currentPromise) =>
      promiseAccumulator.then((resultAccumulator) =>
        currentPromise.then((currentResult) => [...resultAccumulator, currentResult])
      ),
    Promise.resolve([])
  );
}

The types for function signature for Promise.all look like this:

all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

Because I'd like my function to act in a similar way I think the signature should look like:

promiseAllSequential<FunctionsTuple extends readonly (() => unknown)[] | []>(
  functions: FunctionsTuple
): Promise<{
  -readonly [TupleIndex in keyof FunctionsTuple]: Awaited<ReturnType<FunctionsTuple[TupleIndex]>>;
}>

However, I am struggling to get these types too work with the code I've written. I think the main issue is how to type my reduce function when working with a tuple.

I've created a typescript playground which shows the errors. And a typescript playground which shows my best effort to resolve the errors using a lot of hacky assertions.

Is there a feature of typescript I could use to achieve this without having to resort to loads of type assertions?

user9540018
  • 507
  • 3
  • 11
  • Maybe you want to use an external signature like [this](https://tsplay.dev/WKOgDW) and provide `reduce` a type parameter? – kelsny Nov 03 '22 at 16:01
  • That is certainly simpler than my solution thanks @jcalz! Yeh I was thinking the tuple is too complex a structure for the compiler to understand the different types each time the callback is called. I've not seen this `[...F]` syntax before. Do you know of a link I could read up about that? – user9540018 Nov 03 '22 at 16:14
  • @jcalz I can't see anything you are missing. If you were able to write up an answer I would really appreciate it. – user9540018 Nov 03 '22 at 16:21
  • @jcalz that link was really helpful! I just don't understand why typescript document some key features only in the release notes. Oh well. – user9540018 Nov 03 '22 at 16:30
  • I'll write up an answer when I get a chance. The sad fact about the documentation is that the language is being developed more quickly than it can be properly documented, let alone explicitly specified (there's no official language spec). The TS team has said "the release notes & GitHub are part of the documentation." Maybe someday the language will stabilize enough that they could spend more time on specs and docs (although part of the reason I love the language is the regular introduction of awesome new features) – jcalz Nov 03 '22 at 17:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249317/discussion-between-user9540018-and-jcalz). – user9540018 Nov 04 '22 at 10:28

1 Answers1

1

Not only can't TypeScript infer the types necessary to verify the correctness of what you're doing (using the reduce() array method to successively build up a tuple type which ends up as the mapped tuple type you are returning), there's not even a way to express that operation in general. The call signature for reduce() is currently only able to capture the effects of an accumulator of a single type. In order to express the general idea of an evolving accumulator type, you'd need higher kinded types of the sort requested in microsoft/TypeScript#1213. Something like:

// not valid TypeScript, don't try this
interface Array<T> {
  reduce<A extends any[], F<~ extends A['length'] | LessThan<A['length']>>>(
    this: A, 
    callbackfn: <I extends LessThan<A['length']>>(
      previousValue: F<I>, 
      currentValue: A[I], 
      currentIndex: I, 
      array: A
    ) => F<A['length']>, 
    initialValue: F<0>
  ): F<A['length'];
}

where F<~> is some hypothetical higher order type parameter where F can be substituted with a generic type of one type parameter (like Array) and LessThan<T> is a generic type producing a union of non-negative numbers less than its input (you actually can implement this, but that's out of scope here), and A is the tuple type of this.

All of that would at least express that callbackfn takes an accumulator of type F<0> and A[0] and produces a value of type F<1>, and takes an F<1> and A[1] and produces an F<2>, all the way to something that produces an F<A['length']>.

But there's no direct way to refer to a generic parameter that itself ranges over generic types F<~> without microsoft/TypeScript#1213. There are ways to simulate higher kinded types, but they are not simple to use and would make the above signature for reduce() even more ugly... and even so, I sincerely doubt the compiler would be able to infer F, you'd have to specify it, and then the compiler would probably not be able to understand that any particular callback implementation actually adheres to the required higher order call signature.

All of this is to say that this is well outside the capabilities of the compiler, probably for the foreseeable future. It's a similar (or maybe even the same) issue as what's going on in Is Typescript 4.0+ capable of functions using mapped variadic tuple types?


So you're going to have to use at least one type assertion to loosen the type checking enough to prevent compiler errors (or you'll have to use a moral equivalent like overloads where the implementation is checked more loosely than non-overloaded implementation bodies).

So you didn't do anything wrong, per se. If you want, you could limit the assertions to just one, like this:

async function promiseAllSequential<F extends (() => unknown)[]>(
  functions: readonly [...F]
) {
  return functions.reduce(
    (promiseAccumulator, currentFunction) =>
      promiseAccumulator.then(async (resultAccumulator) => {
        const result = await currentFunction();
        return [...resultAccumulator, result];
      }),
    Promise.resolve<unknown[]>([])
  ) as Promise<{
    -readonly [I in keyof F]: Awaited<ReturnType<F[I]>>;
  }>;
}

Remarks:

  • I've removed the assertion on functions by giving it a single type instead of a union. Presumably you wrote F extends (()=>unknown)[] | [] as a hint for the compiler to infer a tuple type instead of a non-tuple array type. You can get the same effect by making functions of type [...F] instead of just F, using variadic tuple type notation, which (according to the implementing pull request at microsoft/TypeScript#39094) also "can conveniently be used to indicate a preference for inference of tuple types".

  • I've moved the annotation of promiseAccumulator: Promise<unknown[]> to just the generic type specification on Promise.resolve<unknown[]>([]). This doesn't do much other than save a few keystrokes.

  • I've removed the "intermediate" as unknown assertion, which isn't necessary here (it's only necessary in some situations where the compiler really has no idea that the asserted type is related to the value).

  • I've removed the return type annotation and just allow the compiler to infer the return type from the asserted return value.

But all of that is mostly just cosmetic; at its core, you have to just assert to the compiler what you're doing with the type, because it can't infer it for you, nor understand that what you're doing is safe.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360