Home > Software design >  How can I fix "WriteableDraft" TS errors in my redux slice?
How can I fix "WriteableDraft" TS errors in my redux slice?

Time:10-26

I'm am new to TypeScript. I am in the process of converting a rather intricate Redux slice from JS to TS. I'm getting permutations of the same error in virtually all of my reducers. The error is TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft... I will include the full source of the reducer slice with comments to indicate where the errors are showing in my IDE. Could someone please point me in the right direction as far as how I can remediate this? Thanks in advance!

Update: After receiving a very helpful reply from @ij7 I was able to fix most of the errors. However, I still have a few that I'm struggling with. Here is the updated code and errors:

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

export type Column = {
  status: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
  title: 'To do' | 'In progress' | 'Done'
  taskIds: [string]
}

export type Columns = {
  [key in 'TO_DO' | 'IN_PROGRESS' | 'DONE']: Column
}

export type Task = {
  id: string
  title: string
  description?: string
  status: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
  board: string,
  column: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
}

export type Tasks = {
  [key: string]: Task
}

interface TaskPayload {
  task: Task
}

interface DeleteTaskPayload {
  column: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
  id: string
}

interface MoveTaskPayload {
  taskId: string
  sourceIndex: number
  sourceColumn: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
  destinationIndex: number
  destinationColumn: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
}

interface HydrateTasksPayload {
  board: {
    id: string
    name: string
    description?: string
    TO_DO: [string]
    IN_PROGRESS: [string]
    DONE: [string]
  }
  tasks: [Task]
}

export type TaskSlice = {
  boardId: string
  boardName: string
  boardDescription?: string
  tasks: Tasks
  columns: Columns
  columnOrder: ['TO_DO', 'IN_PROGRESS', 'DONE']
}

export const  reduceTasks = (tasks: [Task]) => {
  return tasks.reduce((acc, curr) => {
    return  {
      ...acc,
      [curr.id] : {
        ...curr
      }
    }
  }, {})
}

export const initialState = {
  boardId: '',
  boardName: '',
  boardDescription: '',
  tasks: {},
  columns: {
    TO_DO: {
      status: 'TO_DO',
      title: 'To do',
      taskIds: []
    },
    IN_PROGRESS: {
      status: 'IN_PROGRESS',
      title: 'In progress',
      taskIds: []
    },
    DONE: {
      status: 'DONE',
      title: 'Done',
      taskIds: []
    }
  },
  columnOrder: ['TO_DO', 'IN_PROGRESS', 'DONE']
}


const taskSlice = createSlice({
  name: 'task',
  initialState,
  reducers: {
    addTask (state, { payload }: PayloadAction<TaskPayload>) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        },
        columns: {
          ...state.columns,
          [task.status]: {
            ...state.columns[task.status],
            taskIds: [
              ...state.columns[task.status].taskIds,
              task.id
            ]
          }
        }
      }
    },
    deleteTask (state, { payload }: PayloadAction<DeleteTaskPayload>) {
      const { column, id } = payload
      const { [id]: deleted, ...restTasks }: Tasks = state.tasks
      return {
        ...state,
        tasks: {
          ...restTasks
        },
        columns: {
          ...state.columns,
          [column]: {
            ...state.columns[column],
            taskIds: [
              ...state.columns[column]?.taskIds?.filter((taskId:string) => taskId !== id)
            ]
          }
        }
      }
    },
    editTask (state, { payload }: PayloadAction<TaskPayload>) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        },
        columns: {
          ...state.columns,
          [task.column]: {
            ...state.columns[task.column],
            taskIds: [
              ...state.columns[task.column].taskIds
            ]
          }
        }
      }
    },
    replaceTask (state, {payload}: PayloadAction<TaskPayload>) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        }
      }
    },
    moveTask (state, { payload }: PayloadAction<MoveTaskPayload>) {
      const {
        taskId,
        sourceIndex,
        sourceColumn,
        destinationIndex,
        destinationColumn
      } = payload
      const sourceTaskIdsClone = [...state.columns[sourceColumn].taskIds] as string[]

      if (sourceColumn === destinationColumn) {
        sourceTaskIdsClone.splice(sourceIndex, 1)
        sourceTaskIdsClone.splice(destinationIndex, 0, taskId) 
        return {
          ...state,
          columns: {
            ...state.columns,
            [sourceColumn]: {
              ...state.columns[sourceColumn],
              taskIds: sourceTaskIdsClone
            }
          }
        }
      } else {
        sourceTaskIdsClone.splice(sourceIndex, 1)
        const destinationTaskIdsClone = [...state.columns[destinationColumn].taskIds]
        destinationTaskIdsClone.splice(destinationIndex, 0, taskId) //TS2345: Argument of type 'string' is not assignable to parameter of type 'never' (error is on `taskId)
        return {
          ...state,
          tasks: {
            ...state.tasks,
            [taskId]: {
              ...state.tasks[taskId], // TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'WritableDraft<{}>'. No index signature with a parameter of type 'string' was found on type 'WritableDraft<{}>'.
              column: destinationColumn,
              status: initialState.columns[destinationColumn].status
            }
          },
          columns: {
            ...state.columns,
            [sourceColumn]: {
              ...state.columns[sourceColumn],
              taskIds: sourceTaskIdsClone
            },
            [destinationColumn]: {
              ...state.columns[destinationColumn],
              taskIds: destinationTaskIdsClone
            }
          }
        }
      }
    },
    hydrateTasks (state, { payload }: PayloadAction<HydrateTasksPayload>) {
      const { board, tasks } = payload
      const { TO_DO, IN_PROGRESS, DONE } = board
      return {
        boardId: board.id,
        boardName: board.name,
        boardDescription: board.description,
        tasks: reduceTasks(tasks),
        columnOrder: ['TO_DO', 'IN_PROGRESS', 'DONE'],
        columns: {
          TO_DO: {
            status: 'TO_DO',
            title: 'To do',
            taskIds: [...TO_DO]
          },
          IN_PROGRESS: {
            status: 'IN_PROGRESS',
            title: 'In progress',
            taskIds: [...IN_PROGRESS]
          },
          DONE: {
            status: 'DONE',
            title: 'Done',
            taskIds: [...DONE]
          }
        }
      }
    }
  }
})

export const { addTask, deleteTask, editTask, moveTask, addBoard, editBoard, hydrateTasks, replaceTask } = taskSlice.actions
export default taskSlice.reducer

CodePudding user response:

The main problem seems to be that you're not telling the compiler the types of the actions that your reducers are expecting.

Let's use the first one as an example:

const taskSlice = createSlice({
  name: 'task',
  initialState,
  reducers: {
    addTask (state, { payload }) {
      const { task } = payload
      return // ...
    }
    // ...
  }
}

and specifically these two lines:

    addTask (state, { payload }) {
      const { task } = payload

In that function signature, the compiler can correctly infer the type of state because it knows that this is a reducer, but you have to tell it the type of the payload in your action. Without being explicit about that, the compiler doesn't know what the type of payload (and by extension, task) is.

If you add this:

interface TaskPayload {
    task: Task;
}

// ...

    addTask (state, { payload } : PayloadAction<TaskPayload>) {
      const { task } = payload

(where PayloadAction is another import from @reduxjs/toolkit), the compiler now knows that task is a Task, and therefore it knows that state.columns[task.status] is valid.

This gets rid of the first two "... expression of type 'any' can't be used ..." errors; the others are similar.

Aside from getting rid of those errors, you're also making sure that your actions are correctly typed, meaning now the compiler can validate that in

store.dispatch(taskSlice.actions.addTask({ task: whatever }))

the value whatever is actually a Task, and complain if it's not.

  • Related