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

The script resource is experiencing a redirect that is not permitted - Service Worker

I have integrated a Service Worker into my Angular application, and it works perfectly when running on localhost. However, when I attempt to deploy the code in a development or different environment, I encounter the following error: Service worker registra ...

Unable to assign user roles in next-auth due to the absence of matching modifiers for user

I am currently working on implementing user roles in next-auth. Within my database, I have defined a prisma enum UserRole with the values 'ADMIN' and 'USER'. In my auth.ts file, I included the role property in the session object and enc ...

Is it possible to execute "green arrow" unit tests directly with Mocha in IntelliJ IDEA, even when Karma and Mocha are both installed?

My unit tests are set up using Karma and Mocha. The reason I use Karma is because some of the functionality being tested requires a web browser, even if it's just a fake headless one. However, most of my code can be run in either a browser or Node.js. ...

A guide to using Angular to emphasize text based on specific conditions met

Currently, I am developing a testing application that requires users to choose radio type values for each question. The goal is to compare the selected answers with the correct answers stored in a JSON file. To achieve this, I have implemented an if-else ...

After encountering an error, the puppeteer promptly shuts down the page

During my page testing, an error is thrown by a dependency. Although the error is not critical and does not impact my application, when testing with Puppeteer and encountering this error, it abruptly closes the tested page. How can I bypass this error to c ...

Challenges Faced with Implementing Active Reports in Angular 9

After following all the necessary steps outlined in this website to integrate Active Reports with Angular 9 (), I encountered an error when trying to compile my app: ERROR in The target entry-point "@grapecity/activereports-angular" has missing dependen ...

Encountering Syntax Errors during Angular 6 production build

I encountered an issue with my project. Everything was running smoothly, and even when I executed the command ng build --prod, it compiled successfully and generated the dist folder in my project directory. However, after copying this folder to the htdoc ...

Exploring dependency injection in Angular 1 using a blend of JavaScript and TypeScript

I'm currently working on integrating TypeScript into an existing Angular 1.5 application. Despite successfully using Angular services and third-party services, I am facing difficulties in injecting custom services that are written in vanilla JavaScrip ...

Issue with displaying decimal places in Nivo HeatMap

While utilizing Nivo HeatMap, I have observed that the y value always requires a number. Even if I attempt to include decimal places (.00), it will still trim the trailing zeros and display the value without them. The expected format of the data is as foll ...

Solving the issue of loading Ember Initializers during the transition to TypeScript

While following the ember quick start tutorial, I attempted to switch from Javascript to Typescript. Converting the .js files to .ts files resulted in an error with the ember-load-initializers import. The application is unable to run until this issue is re ...

Ensure data accuracy by triggering the cache - implementing SWR hook in Next.js with TypeScript

I recently implemented the swr hook in my next.js app to take advantage of its caching and real-time updates, which has been incredibly beneficial for my project (a Facebook clone). However, I encountered a challenge. The issue arises when fetching public ...

Encountering a navCtrl problem in Ionic 3 while attempting to utilize it within a service

I am currently working on a feature to automatically route users to the Login Page when their token expires. However, I am encountering an issue with red lines appearing under certain parts of my code. return next.handle(_req).do((event: HttpEvent< ...

Is it possible that Typescript does not use type-guard to check for undefined when verifying the truthiness of a variable?

class Base {} function log(arg: number) { console.log(arg); } function fn<T extends typeof Base>( instance: Partial<InstanceType<T>>, key: keyof InstanceType<T>, ) { const val = instance[key]; if (val) { ...

how can I display the JSON description based on the corresponding ID using Ionic 3

I have a JSON file containing: [{ "id": "1", "title": "Abba Father (1)", "description": "Abba Abba Father." }, { "id": "2", "title": "Abba Father, Let me be (2)", "description": "Abba Father, Let me be (2) we are the clay." }, { ...

Unable to locate the name 'Cheerio' in the @types/enzyme/index.d.t file

When I try to run my Node application, I encounter the following error: C:/Me/MyApp/node_modules/@types/enzyme/index.d.ts (351,15): Cannot find name 'Cheerio'. I found a suggestion in a forum that recommends using cheerio instead of Cheerio. H ...

The 'any' type is not compatible with constructor functions

I am currently working on implementing a class decorator in Typescript. I have a function that accepts a class as an argument. const createDecorator = function () { return function (inputClass: any) { return class NewExtendedClass extends inputClass ...

Combine two arrays of data sources

mergeThreads() { const userId = this.auth.getUser().uid; const buyerThreads$ = this.afs.collection('threads', ref => ref.where('buyerId', '==', userId)).valueChanges(); const sellerThreads$ = this.afs.collection ...

After submitting a multi-image form from Angular, the "req" variable is not defined

I'm currently facing an issue with submitting a form from Angular 7 to a Node backend using Multer as middleware and Express.json() as bodyParser. While the text data is successfully transmitted to the backend, the image fields are appearing empty {}. ...

Having an issue with displaying the country name and country code in a table using the Angular7 custom pipe

country code: "ab", "aa", "fr", ... I need to create a custom pipe that will convert a countryCode into a countryName, such as: "ab" → "Abkhazian", "ch" → "Chinese", "fr" ...

Encountering errors in Visual Studio when trying to work with node_modules directories that have a tsconfig

In my current project, there is a tsconfig.json file located in the root directory. Strangely, Visual Studio keeps throwing errors related to other instances of tsconfig.json found in different packages, as shown below: Even though I have excluded node_mo ...