Issue
I have a function that takes multiple reducers and applies them all to a data structure. For example, given two people person1 and person2 it can use this function to normalise their data:
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
},
},
})
Which will return something like:
{
byId: {
1: {
id: 1,
name: "John",
},
2: {
id: 2,
name: "Jane",
},
},
list: [
{
id: 1,
name: "John",
},
{
id: 2,
name: "Jane",
},
],
}
I am having trouble implementing these types in Typescript because I want each property that I pass in to use the type of initialValue as the type for the accumulator acc in the reduce callback.
Is it possible to have this sort of inferred generic type on a mapped type?
Here is a link to 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
},
},
})
Solution
Let's start with the function declaration. I think you can remove the ReducerKeys generic type. We can instead rename InitialValue to InitialValues to indicate that we will store an object type inside there with each key being a specific InitialValue.
export function normalizeData<Model, InitialValues>(
data: Model[],
reducers: {
[K in keyof InitialValues]: {
reduce?: ReduceCallback<Model, InitialValues[K]>
initialValue: InitialValues[K]
}
},
) { /* ... */ }
Instead of mapping over ReducerKeys, we map over InitialValues and store the type of initialValue inside InitialValues[K].
This already fixed the typing when calling the function.
We now get an error inside the function implementation because RecucerKeys does not exist anymore. Let's just replace it with InitialValues too.
const initialValues = reducerKeys.reduce(
(obj, key) => ({
...obj,
[key]: reducers[key].initialValue,
}),
{} as { [K in keyof InitialValues]: InitialValues[K] },
)
Answered By - Tobias S.
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.