DEV Community

Pedro S
Pedro S

Posted on • Edited on

TypeScript: adjusting types in reduce function with an async callback

This took me some type of debugging after not finding a StackOverflow answer that addressed the exact same issue I had, so I thought it would be nice to register my solution.

In TypeScript, I was using the Array.prototype.reduce() method to iterate on an array and populate an object based on an asynchronous function, like this:

function makeObject(keys: string[]) {
  return keys.reduce(async (acc, key) => {
    acc[key] = await asyncFunc(key);
    return acc;
  }, {})
}
Enter fullscreen mode Exit fullscreen mode

Trying to fix the error

The transpiler complained about the third line, when setting a property to acc:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'.

As far as I got this, this happened because the {} object has no type annotation at all, so the acc variable in the callable function would be identified with the any type.

First, I tried to fix it like this:

function makeObject(keys: string[]) {
  return keys.reduce(async (acc, key) => {
    acc[key] = await asyncFunc(key);
    return acc;
  }, <Record<string, string[]>>{})
}
Enter fullscreen mode Exit fullscreen mode

No success; now, there's an error in the declaration of the callback function:

No overload matches this call.
Overload 1 of 3, '(callbackfn: (previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string, initialValue: string): string', gave the following error.
Argument of type '(acc: Record, key: string) => Promise>' is not assignable to parameter of type '(previousValue: string, currentValue: string, currentIndex: number, array: string[]) => string'.
Types of parameters 'acc' and 'previousValue' are incompatible.
Type 'string' is not assignable to type 'Record'.
Overload 2 of 3, '(callbackfn: (previousValue: Record, currentValue: string, currentIndex: number, array: string[]) => Record, initialValue: Record): Record<...>', gave the following error.
Argument of type '(acc: Record, key: string) => Promise>' is not assignable to parameter of type '(previousValue: Record, currentValue: string, currentIndex: number, array: string[]) => Record'.
Type 'Promise>' is not assignable to type 'Record'.
Index signature is missing in type 'Promise>'.

Well, this looks scary. But this basically means that the transpiler:

  • First tried to match the type of acc with the string type of the elements in the keys array. This doesn't make sense in my function, because I'm providing an initial value and returning it from the callback, so the acc variable will never directly receive an element in the array.
  • Then there's a second option (a second "overload"): the return value of my callback function should match the type of acc, which is what the function returns. As the callback is an asynchronous function, it always returns a Promise, and acc is not a Promise, it is a Record<string, string[]>. So I have to change the return value to a promise, and that is the rule for every async function in TypeScript.

(I'm curious about overload 3 of 3 not showing up.)

Next attempt:

function makeObject(keys: string[]) {
  return keys.reduce(async (acc, key) => {
    acc[key] = await asyncFunc(key);
    return acc;
  }, <Promise<Record<string, string[]>>>{})
}
Enter fullscreen mode Exit fullscreen mode

Again, no luck with assigning the new property to acc:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Promise>'.
No index signature with a parameter of type 'string' was found on type 'Promise>'.

The issue is that the type of acc is, now, a full Promise. It makes no sense to plainly add a new property to it; it is like doing this:

>> let aPromise = Promise.resolve({})
<- undefined

>> aPromise['newProp'] = 'hey'
<- 'hey'

>> aPromise
<- Promise { <state>: "fulfilled", <value>: {} }

>> aPromise.newProp
<- 'hey'
Enter fullscreen mode Exit fullscreen mode

The value of the Promise is still empty: I never assigned newProp to it, I only did it to the Promise wrapper.

Solution

This did the trick:

function makeObject(keys: string[]) {
  return keys.reduce(async (acc, key) => {
    const values = await asyncFunc(key);
    acc.then(obj => obj[key] = values);
    return acc;
  }, Promise.resolve(<Record<string, string[]>>{}))
}
Enter fullscreen mode Exit fullscreen mode

This satisfies all the conditions:

  • The async callback function is returning a Promise, as it is supposed to be;
  • I am assigning a property to the value of the Promise, and the value is an object with a specific type (Record<string, string>).

As the Promise is resolved since the beginning, the function inside the acc.then(...) call is guaranteed to always run.

Types are fun.

Top comments (1)

Collapse
 
arnaudcourtecuisse profile image
Arnaud Courtecuisse

Another alternative, that I use :

const final = await arr.reduce(async (promise, key) => { 
  const partial = await promise
  partial[key] = await asyncFn(key)
  return partial
}, {})
Enter fullscreen mode Exit fullscreen mode