Reputation: 3623
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
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
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;
}
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
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
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
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