Home > database >  How can I fix these TypeScript Element implicitly has an 'any' type error in my reducer sl
How can I fix these TypeScript Element implicitly has an 'any' type error in my reducer sl

Time:10-27

I am in the process of converting a rather convoluted Redux reducer slice from JS to TS. I have fixed most of the errors from help I found here and from a course I am taking online. Still, two errors persist. Here is the code of the reducer slice with comments to indicate the errors I see in my IDE:

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

export type Column<T extends string> = {
  status: T
  title: 'To do' | 'In progress' | 'Done'
  taskIds: string[]
}

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

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

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

interface TaskPayload {
  task: Task
}

interface EditTaskPayload {
  task: {
    boardId: string
    column: 'IN_PROGRESS' | 'TO_DO' | 'DONE'
    description?: string
    id: string
    status: 'IN_PROGRESS' | 'TO_DO' | 'DONE'
    title: string
  }
}

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: {
    id: string
    title: string
    description?: string
    status: 'TO_DO' | 'IN_PROGRESS' | 'DONE'
    board: string
  }[]
}

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<EditTaskPayload>) {
      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] as string[]
        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 '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>) { // Types of property 'boardDescription' are incompatible.Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
      const {board, tasks} = payload
      const {TO_DO, IN_PROGRESS, DONE} = board
      if (typeof board.id !== 'undefined') {
        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]
            }
          }
        }
      } else {
        return {
          boardId: board.id,
          boardName: board.name,
          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

The second error has been edited for concision as it is way too long. I have tried to get around the second error by only adding the boardDescription if typeof board.description !== 'undefined' I believe this is called a type guard? That did not change anything. I do not know what to try for the first error. I will continue studying TS online. If anyone can point me in the right direction I would be most grateful. Thanks in advance!

CodePudding user response:

There are three typing issues in your code.

First, on hydrateTasks you're getting a long compiler error that ends with

        Types of property 'boardDescription' are incompatible.
          Type 'string | undefined' is not assignable to type 'string'.
            Type 'undefined' is not assignable to type 'string'.(2322)

which refers to two places, namely this line:

boardDescription: board.description,

and the else block.

This is because state.boardDescription is a string, but on HydrateTasksPayload, board.description is optional, and in the else block you're not setting it at all.

So you either have to remove the question mark, i.e. making board.description required in the action, or provide a fallback for the case that the action doesn't specify a description, e.g. keeping it as is by taking it from the base state:

boardDescription: board.description ?? state.boardDescription,

(what you pick here depends on your desired semantics). This needs to happen in both if branches.

Second, in EditTaskPayload you have a task property called boardId, but it has to be board (matching Task).

Third, the remainder of your errors come from the fact that you don't explicitly provide the interface of your state object, and instead let the compiler infer it from your initialState.

That is not a problem per se, but you have to give the compiler a few more pieces of information.

It means you either have to define an interface State { ... } and say const initialState: State = { ... }, or you add this information directly in the definition of initialState.

tasks: {},

That ^ tells the compiler that initialState.tasks is an empty object. It doesn't know that you mean for it to be a map from IDs to Task objects, you have to tell it that:

tasks: {} as Tasks,

And your column objects (all three of them, I'm just using one as an example) have two of those problems.

    TO_DO: {
      status: 'TO_DO',
      title: 'To do',
      taskIds: []
    },

taskIds is an empty array -- the compiler doesn't know that it's meant to be a string array.

And status: 'TO_DO' makes the compiler widen status to a string, any string. But you mean for it to be that one specific string, always. There's a couple of different ways to express that; the simplest one is as const.

    TO_DO: {
      status: 'TO_DO' as const,
      title: 'To do',
      taskIds: [] as string[]
    },

(You might run into a similar issue with title, because elsewhere you've also typed that as a union type, but currently your code doesn't violate the typings in that regard).

With those changes your code now compiles.

  • Related