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:
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!
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