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;
}, {})
}
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[]>>{})
}
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 thestring
type of the elements in thekeys
array. This doesn't make sense in my function, because I'm providing an initial value and returning it from the callback, so theacc
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 aPromise
, andacc
is not aPromise
, it is aRecord<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[]>>>{})
}
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'
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[]>>{}))
}
This satisfies all the conditions:
- The
async
callback function is returning aPromise
, 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)
Another alternative, that I use :