Search code examples
typescripttypechecking

Type Checking Changed Function Signature in an Object


Issue

I'm creating a wrapper for redux-starter-kit to make it feel more like Vuex. The Github repo is here I'm working on this branch. This project is an experiment. The issue is I can not get these reducers type-checked correctly.

Question

If you have an object of functions like so.

const reducers = {
  addCounter(state:IState, payload:IActionPayload<number>):void {
    state.counter += payload.payload;
  }
}

And it goes through another function and takes the first parameter away. How do you keep the payload Type-checked?

I can do this with generics on a single function and typecast the return

enter image description here

I'm not sure how to do this dynamically for every function in an object.


Solution

  • After hacking away all day. I learned a lot about TypeScript Types and wow is it powerful and ugly. I'm still new to this so if someone has a better option or if I'm misunderstanding something please correct me.

    Steps

    1. Remove the first parameter in a function and move the second to the first position with retaining the type.
    2. Loop over an object and apply step 1 to each item.
    3. We will need to extract the type we are passing into IActionPayload<number>

    Step 1 - Function Changes

    // Formats a Reducer Function to remove state and place the 
    // payload parameter as the first parameter
    type ReducerToAction <R> = R extends (...args: infer A) => any ? (payload:A[1]) => any : never
    

    It works but it sure is ugly. It is all about conditionals. If it is a function capture the args as A with the type of unknown[] but because it is an array we can say A[1] and get our payload. So if it is a function return type (payload:A[1]) => any else never

    Step 2 - Loops

    You can loop over objects with this.

    type loop<obj> = { [key in keyof obj]: obj[key] }
    

    Now we need to do Step1 to obj[key]

    type loop<obj> = { [key in keyof obj]: ReducerToAction<obj[key]> }
    

    So it looks something like this.

    type ReducersToActions<R> = { [K in keyof R]: ReducerToAction<R[K]> }
    

    Step 3 - Extracting the IActionPayload<number> type

    If you provide the type of what you are handling like so.

    type IActionPayload <T> = { type: string, payload: T }
    

    It seems you can access the keys. So if we place this also into a conditional we can pull the single key from the object.

    type PullPayloadType<P> = P extends IActionPayload<any> ? P['payload'] : never
    

    In English is should say if it is type IActionPayload we know it has the key payload so pull it else return never.

    Result

    With this, we will need to update the other types to take this in which will give you the following.

    // Action Structure
    type IActionPayload <T> = { type: string, payload: T }
    // Reducers object structure
    type IReduces = { [key:string] : (state:any, payload:IActionPayload<any>) => any }
    // Gets the Payload type from an object that is of type IActionPayload
    type PullPayloadType<P> = P extends IActionPayload<any> ? P['payload'] : never
    // Formats a Reducer Function to remove state and place the payload parameter as the first parameter
    type ReducerToAction<R> = R extends (...args: infer A) => any ? (payload:PullPayloadType<A[1]>) => any : never
    // Formats Reducer Functions in a object that matches the type IReduces
    type ReducersToActions<R extends IReduces> = { [K in keyof R]: ReducerToAction<R[K]> }