Can I create a unique Generic for every Mapped Type in Typescript?

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
    },
  },
})

Answer №1

Firstly, let's address the function declaration. The suggestion here is to eliminate the use of the ReducerKeys generic type. Instead, we can opt for renaming InitialValue to InitialValues, indicating that an object type will be stored within it, with each key representing a specific InitialValue.

export function normalizeData<Model, InitialValues>(
  data: Model[],
  reducers: {
    [K in keyof InitialValues]: {
      reduce?: ReduceCallback<Model, InitialValues[K]>
      initialValue: InitialValues[K]
    }
  },
) { /* ... */ }

Rather than iterating over ReducerKeys, now we iterate over InitialValues, capturing the type of initialValue as InitialValues[K].

This modification has resolved the typing concerns when invoking the function.

The next step involves rectifying an error within the function implementation due to the absence of RecucerKeys. To rectify this, simply replace all instances of RecucerKeys with InitialValues.

const initialValues = reducerKeys.reduce(
  (obj, key) => ({
    ...obj,
    [key]: reducers[key].initialValue,
  }),
  {} as { [K in keyof InitialValues]: InitialValues[K] },
)

To experiment further with these modifications, head over to the Playground

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular 1.5 component causing Typescript compiler error due to missing semi-colon

I am encountering a semi-colon error in TypeScript while compiling the following Angular component. Everything looks correct to me, but the error only appears when I insert the this.$routeConfig array: export class AppComponent implements ng.IComponentOp ...

Consecutive requests to APIs using RxJs

Is it possible to efficiently make sequential API calls using RxJs? The challenge lies in the fact that the first Observable emits an array, and for each item in this array, a custom URL should be set for the next call. Additionally, certain conditions nee ...

Learn how to deactivate the pause button with just one click and re-enable it once the popup appears using Angular2 and Typescript

Can anyone assist with solving an issue I am facing with a timer and a pause button? I need the pause button to be disabled once clicked, until a popup appears, then it should become enabled again. My code snippet is provided below: HTML: <button md-i ...

Implementing indexers in TypeScript to accommodate both string and numeric keys

Seeking to incorporate different types into existing code. In the code, there exists a transitionData object in which objects can be added by index as shown below: this.transitionData[id] = transition where id is a number and transition is of type Trans ...

Step-by-step guide on building a wrapper child component for a React navigator

When using the Tab.Navigator component, it is important to note that only the Tab.Screen component can be a direct child component. Is there a way in Typescript to convert or cast the Tab.Screen Type to the TabButton function? const App = () => { retur ...

Address aliases in the webpack configuration file

When utilizing webpack, it is possible to write the configuration file using TypeScript. However, it is crucial to ensure that any alias paths present in the config file are resolved to their mapped paths. It should be noted that this pertains specificall ...

Typescript struggling to comprehend the conditional rendering flow

I am facing an issue with the code snippet below: import * as React from 'react' type ComponentConfig = [true, {name: string}] | [false, null] const useComponent = (check: boolean): ComponentConfig => { if (check) { return [true, {name ...

Utilizing the URL path name for data retrieval in Next.js 14 - A step-by-step guide

I'm currently developing a blog using AWS Amplify Gen 2 and GraphQL for a Next.js 14 project with TypeScript. As part of my application, I need to fetch specific data based on the URL path name. Here's how I've approached it: My approach in ...

Is there a way to determine the quantity of lines within a div using a Vue3 watcher?

Is it feasible to determine the number of text lines in a div without line breaks? I am looking to dynamically display or hide my CTA link based on whether the text is less than or equal to the -webkit-line-clamp value: SCRIPT: const isExpanded = ref(true ...

When employing GraphQL Apollo refetch with React, the update will extend to various other components as well

My current setup involves using react along with Apollo. I have implemented refetch in the ProgressBar component, which updates every 3 seconds. Interestingly, another component named MemoBox also utilizes refetch to update the screen at the same int ...

Exclude the key-value pair for any objects where the value is null

Is there a way to omit one key-value pair if the value is null in the TypeScript code snippet below, which creates a new record in the Firestore database? firestore.doc(`users/${user.uid}`).set({ email: user.email, name: user.displayName, phone: ...

Successive type label

Looking to create an object that can have either primitives or objects as properties? Avoid pitfalls like the following: const obj: DesiredType = { correctProp1: 'string', correctProp2: 123, correctProp3: true, wrongProp4: [1, 2, 3], pr ...

The "rest" variable is automatically assigned the type of "any" because it lacks a specified type and is used within its own initializer

Attempting to set up a private route using react router 4 and Typescript. Check out the code I'm working with: type CustomRouteProps<T> = T & { component: any, authRequired: boolean }; function PrivateRoute({ component: Component, authRequ ...

Tips for updating the value within a textfield in HTML

I am looking to dynamically update the value displayed in my Revenue textfield by subtracting the Cost of Goods from the Sales Price. I have included an image of the current layout for reference, but I want the Revenue field to reflect the updated value af ...

What is the best way to pause function execution until a user action is completed within a separate Modal?

I'm currently working on a drink tracking application. Users have the ability to add drinks, but there is also a drink limit feature in place to alert them when they reach their set limit. A modal will pop up with options to cancel or continue adding ...

Utilize Typescript to Invoke Functions of Different Components in Angular 2

Hello everyone, I am a newcomer to Angular 2 and I'm looking to utilize the value of one component in another component. This will help me populate data based on that particular value. In my setup, I have three Components - App.Component, Category.Co ...

Tips for effectively sending prop to a component in React with the help of TypeScript

Hey there, I'm working on a component called FormField which can accept either an icon for create or edit. Currently, I am using this FormField inside another component called SelectWithFormField. Here's how it looks: const FormField = ({create, ...

Tips for Implementing Error Handling in Angular using Sweetalert2

On this code snippet, I have implemented a delete confirmation popup and now I am looking to incorporate error handling in case the data is not deleted successfully. confirmPopUp(){ Swal.fire({ title: 'Are You Sure?', text: 'Deleti ...

IntelliJ IDEA does not support the recognition of HTML tags and directives

I seem to have lost the ability to switch between my HTML and TS files in Intellij IDEA; the tags, directives, and autocompletion in HTML are no longer working. Additionally, I'm receiving some warnings: https://i.stack.imgur.com/QjmNk.png Is there ...

Is there a way to integrate TypeScript with styled components to display suggested properties on the component?

Hey there fellow developers! I'm currently diving into the world of TypeScript and trying to get the hang of it. One thing that's bothering me is not being able to see recommended props on a styled component while using TypeScript. For instance ...