Kid_Learning_C
Kid_Learning_C

Reputation: 3623

Type 'null' cannot be used as an index type in Array.reduce() accumulator

This is a follow-up question to: Javascript: How to convert a list of objects with many key-value pairs into one big nested object?

The original goal was to convert a list of objects with many key-value pairs into one big nested object.

for example, from this:

const items = [
        {
            "id": 3,
            "orgId": 2,
            "mod": "toyota",
            "part": "wheel",
            "price": 333
        },
        {
            "id": 4,
            "orgId": 2,
            "mod": "toyota",
            "part": "shell",
            "price": 350
        },
        {
            "id": 9,
            "orgId": 2,
            "mod": "honda",
            "part": "wheel",
            "price": 222
        },
        {
            "id": 10,
            "orgId": 2,
            "mod": "honda",
            "part": "shell",
            "price": 250
        }
    ]


and convert to:


items = {
    "toyota": {"wheel": 333, "shell": 350 }, 
    "honda": {"wheel": 222, "shell": 250 }
}

The following code works in Javascript:

const transformedItems = items.reduce((acc, item) => {
  acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
  return acc
}, {})

console.log(transformedItems)

How, I want to put this logic to server-side (written in Typescript), and the code does not compile:

/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:293
    return new TSError(diagnosticText, diagnosticCodes)
           ^
TSError: ⨯ Unable to compile TypeScript:
src/utils/billingFunctions.ts:52:11 - error TS2538: Type 'null' cannot be used as an index type.

52       acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
             ~~~~~~~~~~~~~
src/utils/billingFunctions.ts:52:37 - error TS2538: Type 'null' cannot be used as an index type.

52       acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
                                       ~~~~~~~~~~~~~
src/utils/billingFunctions.ts:52:53 - error TS2464: A computed property name must be of type 'string', 'number', 'symbol', or 'any'.

52       acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
                                                       ~~~~~~~~~~~~~~~

    at createTSError (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:293:12)
    at reportTSError (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:297:19)
    at getOutput (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:399:34)
    at Object.compile (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:457:32)
    at Module.m._compile (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:530:43)
    at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/john/tmp/dolphin/api/node_modules/ts-node/src/index.ts:533:12)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
[nodemon] app crashed - waiting for file changes before starting...


However, when I try:

const transformedItems = items.reduce((acc, item) => {

  console.log(acc, item.mod)  // print out

  acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
  return acc
}, {})

The console log prints normally: item.mod is a string. How come it complains that it is Type 'null'?

Upvotes: 0

Views: 1674

Answers (5)

MaxZoom
MaxZoom

Reputation: 7763

Based on the error message, there is a record that does not have any value in the item.mod field.

Type 'null' cannot be used as an index type for acc[item.mod] =

The solution depends on what are you going to do with such a record.

If you would like to keep it then cast the item.mod to String as other answers suggested.

If you would like to omit it then you need to add a check as below

const transformedItems = items.reduce((acc, item) => {
  if (item.mod) {
    acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
  }
  return acc
}, {})

In both cases, it seems the data you are handling is inconsistent and has to be validated before use.

Upvotes: 1

Alex Bagatka
Alex Bagatka

Reputation: 56

Alternative way, more classic one I would say

Let's add types, it will make transformation easier:

interface CommonData {
    id: number;
    orgId: number;
    mod: string;
    part: string;
    price: number;
}

interface PartDetails {
    name: string;
    price: number;
}

interface TransformedData {
    carModel: string;
    details: Array<PartDetails>
}

Transformation logic:

function transformData(data: Array<CommonData>): Array<TransformedData> {
    const transformedData: Array<TransformedData> = [];

    data.forEach((item: CommonData) => {
        const index = transformedData.findIndex(transformedItem => transformedItem.carModel === item.mod);
        
        if (index === -1) {
            transformedData.push({
                carModel: item.mod,
                details: [
                    {
                        name: item.part,
                        price: item.price
                    }
                ]
            })
        } else {
            const existingTransform = transformedData[index];

            existingTransform.details.push({
                name: item.part,
                price: item.price
            });
        }
    });

    return transformedData;
}

Example

Input

[
    {
        "id": 3,
        "orgId": 2,
        "mod": "toyota",
        "part": "wheel",
        "price": 333
    },
    {
        "id": 4,
        "orgId": 2,
        "mod": "toyota",
        "part": "shell",
        "price": 350
    },
    {
        "id": 9,
        "orgId": 2,
        "mod": "honda",
        "part": "wheel",
        "price": 222
    },
    {
        "id": 10,
        "orgId": 2,
        "mod": "honda",
        "part": "shell",
        "price": 250
    }
]

Output

[
    {
        "carModel": "toyota",
        "details": [
            {
                "name": "wheel",
                "price": 333
            },
            {
                "name": "shell",
                "price": 350
            }
        ]
    },
    {
        "carModel": "honda",
        "details": [
            {
                "name": "wheel",
                "price": 222
            },
            {
                "name": "shell",
                "price": 250
            }
        ]
    }
] 

Upvotes: 1

soimon
soimon

Reputation: 2580

You should make sure that items is an array of Item, not of any. When writing your items array like in your example, TypeScript can automatically deduce that all your items have a mod field that is a string because all examples have one. I'm suspecting you are passing the items as a parameter to a function though (or populating it dynamically), and in that case you should define the type of the items by yourself.

This should give TypeScript no question that every entry inside the items array has a mod and a part field that is definitely a string.

type Item = {
    id: number;
    orgId: number;
    mod: string;
    part: string;
    price: number;
};

function getTransformedItems(items: Item[]) {
    return items.reduce((acc, item) => {
        acc[item.mod] = { ...acc[item.mod], [item.part]: item.price };
        return acc;
    }, {} as Record<string, Record<string, number>>);
}

Upvotes: 1

Tumo Masire
Tumo Masire

Reputation: 432

I think one solution that you could try is too coerce the item.mod so that TypeScript is also sure that it is a string. You would only need to wrap it in String(...) like so:

const transformedItems = items.reduce((acc, item) => {

  console.log(acc, item.mod)  // print out

  acc[item.mod] = { ...acc[String(item.mod)], [item.part]: item.price }
  return acc
}, {})

Not the most TypeScripty way I think.

Upvotes: 1

brunnerh
brunnerh

Reputation: 185589

When using reduce you often have to manually supply the type of the accumulated value, especially when you pass in something like an empty object {}. You can either add a type assertion at the parameter or use the generic parameter of the function:

const transformedItems = items.reduce<Record<string, Record<string, number>>>(
    (acc, item) => {
        acc[item.mod] = { ...acc[item.mod], [item.part]: item.price }
        return acc
    }, {})

If the items variable is passed in dynamically and lacks type information you may also need to define its type, e.g. using an interface:

interface Item
{
    id: number;
    orgId: number;
    mod: string;
    part: string;
    price: number;
}

Which then can be used inline or somewhere before that:

const transformedItems = (items as Item[]).reduce(...

Upvotes: 1

Related Questions