Home > Software design >  Transform array of objects to single object and keep types in TypeScript
Transform array of objects to single object and keep types in TypeScript

Time:09-23

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

CodePudding user response:

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>

CodePudding user response:


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]

Playground

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

  • Related