0

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'?

H.B.
  • 166,899
  • 29
  • 327
  • 400
Kid_Learning_C
  • 2,605
  • 4
  • 39
  • 71
  • https://stackoverflow.com/questions/46043087/type-null-cannot-be-used-as-an-index-type – Hassan Imam Aug 19 '21 at 15:12
  • Can you try giving a type to `array#reduce` as `{} as {[key: string]: any}` ? – Hassan Imam Aug 19 '21 at 15:23
  • What version of TypeScript are you using? I'm seeing a different error. `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 '{}'.` Do all your `items` look like that, or are some missing certain properties? – Connor Low Aug 19 '21 at 15:25

5 Answers5

1

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>>);
}
soimon
  • 2,400
  • 1
  • 15
  • 16
  • @ConnorLow Thanks, this was a red flag that my tooling was set up incorrectly ;) Fixed both the answer and my IDE. – soimon Aug 19 '21 at 15:33
1

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(...
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Casting `items as Item[]` is not secure: `const items:any = "hey"` gets silently cast to `Item[]` as well. If the problem is that `items` is of type `any`, this solution will not solve the error but only disguise it for the type checker. – soimon Aug 19 '21 at 15:39
  • @soimon: Well yes, it depends on where the variable comes from how this should be handled. Ideally the value is checked and typed correctly upstream where it is created. – H.B. Aug 19 '21 at 15:42
1

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.

Tumo Masire
  • 422
  • 5
  • 10
1

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
            }
        ]
    }
] 
1

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.

MaxZoom
  • 7,619
  • 5
  • 28
  • 44