Question:
How to fix issues with TS generics and record type mapping?

Problem

I have an object that looks something like this:


const actions = {

    foo: (state: string, action: {payload: string}) => {},

    bar: (state: string, action: {payload: number}) => {},

    baz: (state: string, action: {payload: boolean}) => {},

    qux: (state: string) => {}

}


And I have a generic type that looks like this:


type ActionMapper<T> = T extends (state: any, action: infer Action) => any 

    ? Action extends {payload: infer P} 

        ? (payload: P) => void 

        : VoidFunction 

    : never


The use case for this generic type works like this:

As you can see, it extracts the type of the action.payload parameter from the actions.foo() function.


I want to have a generic type that would allow me to do the same thing but for the entire actions object.


What have I tried?

I tried creating a generic type that looks like this:


type ActionsMapper<A> = Record<keyof A, ActionMapper<A[keyof A]>>


but it doesn't exactly do what I was expecting:

Instead of only extracting the parameter type from actions.foo() it extracts it from every function and creates a type union. What I want is for the type of mappedActions.foo() to be (payload: string) => void.


Here is the playground I was using: >TypeScript Playground.


Solution

The reason you are getting union types is the Record<keyof A, ActionMapper<A[keyof A]>>: it creates an object with all the keys of A, with each property having the type of ActionMapper applied to all the values of A. What you want, however, is an object with all the keys of A, with each property having the type of ActionMapper applied to the value of that same key of A.


You can do this using a >mapped type:


type ActionsMapper<A> = {

  [K in keyof A]: ActionMapper<A[K]>

}


This creates the wanted object, where the key used in the value for each property is the key for that property, and just that key.


>TypeScript Playground


The difference is that in a mapped type, you can base each value on the key for that property. With Record, all the values are the same.


Given:


type ActionsMapper<A> = Record<keyof A, ActionMapper<A[keyof A]>>


Simplifying leaves the following:


type ARecord<T> = Record<keyof T, T[keyof T]>


Here, keyof T is all the property keys of T, and T[keyof T] is all the property values of T. The two keyof expressions are not linked.


We can simplify the mapped type too:


type AMapped<T> = {

  [K in keyof T]: T[K]

}


And now we can compare the Record-based type with the custom mapped type. You can easily see the difference:


// Record<keyof T, T[keyof T]>

type RecordBased =

  { [K in keyof T]: T[keyof T] }


type Mapped =

  { [K in keyof T]: T[K]       }


Suggested blogs:

>Why Typescript does not allow a union type in an array?

>Narrow SomeType vs SomeType[]

>Create a function that convert a dynamic Json to a formula in Typescript

>How to destroy a custom object within the use Effect hook in TypeScript?

>How to type the values of an object into a spreadsheet in TypeScript?

>Type key of record in a self-referential manner in TypeScript?

>How to get the last cell with data for a given column in TypeScript?

>Ignore requests by interceptors based on request content in TypeScript?

>Create data with Typescript Sequelize model without passing in an id?

>How to delete duplicate names from Array in Typescript?


Nisha Patel

Nisha Patel

Submit
0 Answers