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.