Search code examples
reactjstypescriptreduxredux-toolkit

Creating generic redux slice to update nested initialState object


I am working with Redux Toolkit to create generic slice to update nested initialState object, but unfortunately because I lack some advanced TS knowledge I cannot find a way how I can achieve that. Here's a code snipped of my another try:

TS Playground link

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export enum FIELD_ID {
  FAX = 'fax',
  HOME = 'homePhone',
  WORK = 'workPhone',
  CONTACT = 'contact',
  OTHER = 'other'

}

type GenericFetchState<Data> = {
  data: Data
  isLoading: boolean
  error?: {
    name?: string
    message?: string
    code?: string
    stack?: string
  }
  dataDidInvalidate: boolean
  formId?: string
}

type DefaultState = {
  value: any // We can leave any, not necessary for this issue
  isDirty: boolean
  isValid: boolean
  error: string
}

export type Form = {
  [FIELD_ID.CONTACT]: {
    [FIELD_ID.HOME]: DefaultState
    [FIELD_ID.WORK]: DefaultState
  }
  [FIELD_ID.OTHER]: {
    [FIELD_ID.FAX]: DefaultState
  }
}

type InitialState = GenericFetchState<Form>

export const DEFAULT_INPUT_STATE: DefaultState = {
  value: '',
  isDirty: false,
  isValid: true,
  error: ''
} as const


export const initialState: InitialState = {
  data: {
    [FIELD_ID.CONTACT]: {
      [FIELD_ID.HOME]: DEFAULT_INPUT_STATE,
      [FIELD_ID.WORK]: DEFAULT_INPUT_STATE
    },
    [FIELD_ID.OTHER]: {
      [FIELD_ID.FAX]: DEFAULT_INPUT_STATE
    }
  },
  dataDidInvalidate: true,
  isLoading: false,
  error: null,
  formId: null
}

type SectionKeys = keyof Form
type VarNameMap<T extends SectionKeys> = Form[T]

// I've ended up with creation of Map of section/varName pairs, to let Typescript know the deeper nesting types, but without a success.
type SectionVarNamesMap = {
  [K in SectionKeys]: {
    [J in keyof VarNameMap<K>]: [section: K, varName: J]  //Eg. [section: FIELD_ID.CONTACT, varName: FIELD_ID.HOME]
  }[keyof VarNameMap<K>]
}[SectionKeys]


type PayloadObject<T extends SectionVarNamesMap> = { // This is what I am struggling with
  section: T[0]
  varName: T[1]
  value: any // value property doesn't matter for this example
  error?: string
}

const formSlice = createSlice({
  name: 'form',
  initialState,
  reducers: {
    updateValue: (state, action: PayloadAction<PayloadObject<SectionVarNamesMap>>) => {
      switch (action.payload.section) {
        case FIELD_ID.CONTACT: {
          const varName = action.payload.varName //  It should be narrowed here by switch case, expected output: FIELD_ID.HOME | FIELD_ID.WORK

          // previously I did this, but I have a feeling that's not the right way:

          // const varName = action.payload.varName as keyof Form[FIELD_ID.CONTACT]

          const oldValue = state.data[action.payload.section][varName].value // Error : Property '[FIELD_ID.FAX]' does not exist on type 'WritableDraft<{ homePhone: DefaultState; workPhone: DefaultState; }>'.ts(7053)

          state.data[action.payload.section][varName] = {
            value: action.payload.value ?? oldValue,
            isDirty: true,
            isValid: !action.payload.error,
            error: !!action.payload.error ? action.payload.error : ''
          }

          break
        }

        case FIELD_ID.OTHER: {
          const oldValue = state.data[action.payload.section][action.payload.varName].value //  It should be narrowed here by switch case, expected output: FIELD_ID.FAX

          state.data[action.payload.section][action.payload.varName] = {
            value: action.payload.value ?? oldValue,
            isDirty: true,
            isValid: !action.payload.error,
            error: !!action.payload.error ? action.payload.error : ''
          }
          break
        }
        default: {
          break
        }
      }
    }
  }
})

export const { updateValue } = formSlice.actions
export default formSlice.reducer

For now I've used as keyword inside each switch case to tell TS explicitly what type it should expect, but I feel like it's a wrong way.

Any help is warmly welcome, thanks!


Solution

  • I was actually close to the solution, here it is:

    type SectionVarNamesMap = {
      [K in SectionKeys]: {
        [J in keyof VarNameMap<K>]: [section: K, varName: J]  
      }[keyof VarNameMap<K>]
    } // ! Removed [SectionKeys] from here
    
    type PayloadObject<T extends SectionVarNamesMap> = { 
     [K in SectionKeys]: {
      section: T[K][0]
      varName: T[K][1]
      value: any 
      error?: string
     }
    }[SectionKeys] // Added [SectionKeys] here
    

    TS Playground Link