Reputation: 2073
I have an array of objects with keys name
and value
. I would like to convert this array into single object, where keys are name
and values are value
properties from input objects.
type Input = { name: string, value: any }[]
type Output = Record<string, any> // Key-value object { [name]: value }
const input: Input = [
{ name: 'name', value: 'Michal' },
{ name: 'age', value: 24 },
{ name: 'numbers', value: [4, 7, 9] }
]
const getOutput = (input: Input): Output => {
return input.reduce((output, record) => ({ ...output, [record.name]: record.value }), {})
}
// Output is: { name: 'Michal', age: 24, numbers: [4, 7, 9] }
const output: Output = getOutput(input)
The example above is working, however I used Record<string, any>
type for output. That means I lost types of values. Is there any way to perform this transformation, but keep types?
output.age.length // Should be TS error, `number` has no `length` property
output.numbers.length // 3
output.address // Should be TS error, `input` has no `address` property
Upvotes: 2
Views: 1205
Reputation: 33041
type Elem<V> = { name: string, value: V }
type Callback<Item> =
Item extends { name: infer Name, value: infer Value }
? Name extends PropertyKey
? Record<Name, Value> : never : never
type Reducer<T extends Array<any>, Acc = {}> =
T extends []
? Acc
: T extends [infer Head, ...infer Tail]
? Reducer<Tail, Acc & Callback<Head>>
: never
const getOutput = <
N extends number,
Value extends number | string | [N, ...N[]],
Name extends string,
Item extends { name: Name, value: Value },
Input extends Item[]
>(input: [...Input]) =>
input.reduce((output, record) =>
({ ...output, [record.name]: record.value }),
{} as Reducer<Input>
)
const output = getOutput([
{ name: 'name', value: 'Michal' },
{ name: 'age', value: 24 },
{ name: 'numbers', value: [4, 7, 9] }
])
output.age // 24
output.name // 'MIchal'
output.numbers // [4,7,9]
Explanation
Reducer
and Callback
- works almost exactly like Array.prototype.reducer
, except it iterates recursively.
Here is js representation of Reducer:
const Callback = (elem) => {
const { name, value } = elem;
return { [name]: value }
}
const reducer = (arr: ReadonlyArray<any>, result: Record<string, any> = {}): Record<string, any> => {
if (arr.length === 0) {
return result
}
const [head, ...tail] = arr;
return reducer(tail, { ...result, ...Callback(head) }
}
See this answer and my blog for more information.
[...Input]
- I have used variadic tuple types to infer each object in the array
Upvotes: 4
Reputation: 11173
Your input data structure seems surprising, maybe you should consider changing the way you handle your data
One way you could go to sort the issue while keeping the input value untouched:
type ValueDescriptor = {type:'NUMBER',value:number} | {type:'STRING',value:string} | {type:'NUMBER_ARRAY',value:number[]} /* | ... any business-backed type*/
type Output = Record<string, ValueDescriptor>
Upvotes: 0