I've got a function that accepts multiple reducers and applies them all to a data structure. For instance, it can normalize the data of two individuals person1
and person2
using this function:
normalizeData([person1, person2], {
byId: {
initialValue: {} as { [id: string]: Person },
reduce: (acc, model) => {
acc[model["id"]] = model
return acc
},
},
list: {
initialValue: [] as Person[],
reduce: (acc, model) => {
acc.push(model)
return acc
},
},
})
This will produce something similar to:
{
byId: {
1: {
id: 1,
name: "John",
},
2: {
id: 2,
name: "Jane",
},
},
list: [
{
id: 1,
name: "John",
},
{
id: 2,
name: "Jane",
},
],
}
I'm facing challenges implementing these types in Typescript because I want each property passed in to utilize the type of initialValue
as the type for the accumulator acc
in the reduce callback.
Is it possible to have this inferred generic type on a mapped type?
Click here to access the full code in a runnable example
Reproducible example
// Same type as Array.reduce callback
type ReduceCallback<Value, Output> = (
previousValue: Output,
currentValue: Value,
currentIndex: number,
array: Value[],
) => Output
// Type for sample data
type Person = {
id: string
name: string
parentId?: string
age: number
}
// Function to run multiple reducers over an array of data
// This is the function I want to type properly
export function normalizeData<Model, ReducerKeys extends string, InitialValue>(
data: Model[],
reducers: {
[key in ReducerKeys]: {
reduce?: ReduceCallback<Model, InitialValue>
initialValue: InitialValue
}
},
) {
// Get keys of reducers to split them into two data structures,
// one for initial values and the other for reduce callbacks
const reducerKeys = Object.keys(reducers) as Array<keyof typeof reducers>
// Get an object of { id: <initialValue> }
// In this case `{ byId: {}, list, [] }`
const initialValues = reducerKeys.reduce(
(obj, key) => ({
...obj,
[key]: reducers[key].initialValue,
}),
{} as { [key in ReducerKeys]: InitialValue },
)
// Get an array of reduce callbacks
const reduceCallbacks = reducerKeys.map((key) => ({ key, callback: reducers[key].reduce }))
// Reduce over the data, applying each reduceCallback to each datum
const normalizedData = data.reduce((acc, datum, index, array) => {
return reduceCallbacks.reduce((acc, { key, callback }) => {
const callbackWithDefault = callback || ((id) => id)
return {
...acc,
[key]: callbackWithDefault(acc[key], datum, index, array),
}
}, acc)
}, initialValues)
return normalizedData
}
// Sample data
const parent: Person = {
id: "001",
name: "Dad",
parentId: undefined,
age: 53,
}
const son: Person = {
id: "002",
name: "Son",
parentId: "001",
age: 12,
}
// This is the test implementation.
// The types do not accept differing generic types of initialValue for each mapped type
// Whatever is listed first sets the InitialValue generic
// I want to be able to have the intialValue type for each mapped type
// apply that same type to the `acc` value of the reduce callback.
normalizeData([parent, son], {
byId: {
initialValue: {} as {[key: string]: Person},
reduce: (acc, person) => {
acc[person.id] = person
return acc
},
},
list: {
initialValue: [] as Person[],
reduce: (acc, person) => {
acc.push(person)
return acc
},
},
})