Neil Girardi
Neil Girardi

Reputation: 4923

How can I fix "WriteableDraft" TS errors in my redux slice?

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!

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

export type Tasks = {
  [key: string]: 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 }) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        },
        columns: {
          ...state.columns,
          [task.status]: {
            ...state.columns[task.status], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            taskIds: [
              ...state.columns[task.status].taskIds, // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
              task.id
            ]
          }
        }
      }
    },
    deleteTask (state, { payload }) {
      const { column, id } = payload
      const { [id]: deleted, ...restTasks }: Tasks = state.tasks
      return {
        ...state,
        tasks: {
          ...restTasks
        },
        columns: {
          ...state.columns,
          [column]: {
            ...state.columns[column], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            taskIds: [
              ...state.columns[column]?.taskIds?.filter((taskId:string) => taskId !== id) // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            ]
          }
        }
      }
    },
    editTask (state, { payload }) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        },
        columns: {
          ...state.columns,
          [task.column]: {
            ...state.columns[task.column], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            taskIds: [
              ...state.columns[task.column].taskIds // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            ]
          }
        }
      }
    },
    replaceTask (state, {payload}) {
      const { task } = payload
      return {
        ...state,
        tasks: {
          ...state.tasks,
          [task.id]: task
        }
      }
    },
    moveTask (state, { payload }) {
      const {
        taskId,
        sourceIndex,
        sourceColumn,
        destinationIndex,
        destinationColumn
      } = payload
      const sourceTaskIdsClone = [...state.columns[sourceColumn].taskIds] // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft

      if (sourceColumn === destinationColumn) {
        sourceTaskIdsClone.splice(sourceIndex, 1)
        sourceTaskIdsClone.splice(destinationIndex, 0, taskId)
        return {
          ...state,
          columns: {
            ...state.columns,
            [sourceColumn]: {
              ...state.columns[sourceColumn], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
              taskIds: sourceTaskIdsClone
            }
          }
        }
      } else {
        sourceTaskIdsClone.splice(sourceIndex, 1)
        const destinationTaskIdsClone = [...state.columns[destinationColumn].taskIds] // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
        destinationTaskIdsClone.splice(destinationIndex, 0, taskId)
        return {
          ...state,
          tasks: {
            ...state.tasks,
            [taskId]: {
              ...state.tasks[taskId], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
              column: destinationColumn,
              status: initialState.columns[destinationColumn].status // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
            }
          },
          columns: {
            ...state.columns,
            [sourceColumn]: {
              ...state.columns[sourceColumn], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
              taskIds: sourceTaskIdsClone
            },
            [destinationColumn]: {
              ...state.columns[destinationColumn], // TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'WritableDraft
              taskIds: destinationTaskIdsClone
            }
          }
        }
      }
    },
    hydrateTasks (state, { payload }) {
      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

Upvotes: 1

Views: 1904

Answers (1)

ij7
ij7

Reputation: 369

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.

Upvotes: 1

Related Questions