TypeScript: adjusting types in reduce function with an async callback
Tags: typescript, reduce, async, promises
Published: 2020-11-23 13:59:44

---
title: TypeScript: adjusting types in reduce function with an async callback
published: true
description: Debugging type confusion in an async callback.
tags: typescript, reduce, async, promises
---

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:

```typescript
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:

```typescript
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<string, string[]>, key: string) => Promise<Record<string, string[]>>' 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<string, string[]>'.
>	Overload 2 of 3, '(callbackfn: (previousValue: Record<string, string[]>, currentValue: string, currentIndex: number, array: string[]) => Record<string, string[]>, initialValue: Record<string, string[]>): Record<...>', gave the following error.
>		Argument of type '(acc: Record<string, string[]>, key: string) => Promise<Record<string, string[]>>' is not assignable to parameter of type '(previousValue: Record<string, string[]>, currentValue: string, currentIndex: number, array: string[]) => Record<string, string[]>'.
>			Type 'Promise<Record<string, string[]>>' is not assignable to type 'Record<string, string[]>'.
>				Index signature is missing in type 'Promise<Record<string, string[]>>'.

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:

```typescript
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<Record<string, string[]>>'.
>	No index signature with a parameter of type 'string' was found on type 'Promise<Record<string, string[]>>'.

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:

```javascript
>> 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:

```typescript
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 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.