Search code examples
reactjstypescriptactionredux-thunkredux-toolkit

How do I properly use createAsyncThunk function with typescript?


You can find the complete project here.

I have the following code:

    extraReducers: (builder) => {
        builder
            .addCase(getTodosAsync.fulfilled, (state, action:any) => {
                return action.payload.todos
            })
            .addCase(addTodoAsync.fulfilled, (state, action:any) => {
                state.push(action.payload.todo)
            })
            .addCase(toggleCompleteAsync.fulfilled, (state, action:any) => {
                const index = state.findIndex(
                    (todo) => todo.id === action.payload.todo.id
                )
                state[index].completed = action.payload.todo.completed
            })
            .addCase(deleteTodoAsync.fulfilled, (state, action:any) => {
                return state.filter((todo) => todo.id !== action.payload.id)
            })
    }

but I want to properly type the action parameter of the callbacks,in other words, get rid of the 'any' type. I already found out that the correct way is typing the createAsyncThunk, but until now, I don't know how to do it.

The rest of the file's code is as the following:

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

// import {AsyncThunkFulfilledActionCreator} from '../../node_modules/@reduxjs/toolkit/src/createAsyncThunk'

import { nanoid } from 'nanoid';

interface propsPayload {
    title?: string,
    id?: string,
    completed?: boolean
}


const initialState = [
    {}
] as Array<propsPayload>

export const getTodosAsync = createAsyncThunk(
    'todos/getTodosAsync',
    async () => {
        const resp = await fetch('http://localhost:7000/todos');
        if (resp.ok) {
            const todos = (await resp.json()) ;
            return { todos } ;
        }
    }
);

export const addTodoAsync = createAsyncThunk(
    'todos/addTodoAsync',
    async (payload: propsPayload) => {
        const resp = await fetch('http://localhost:7000/todos', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ title: payload.title }),
        });

        if (resp.ok) {
            const todo = await resp.json();
            return { todo } ;
        }
    }
);

export const toggleCompleteAsync = createAsyncThunk(
    'todos/completeTodoAsync',
    async (payload: propsPayload) => {
        const resp = await fetch(`http://localhost:7000/todos/${payload.id}`, {
            method: 'PATCH',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ completed: payload.completed }),
        });

        if (resp.ok) {
            const todo = await resp.json();
            return { todo } ;
        }
    }
);

export const deleteTodoAsync = createAsyncThunk(
    'todos/deleteTodoAsync',
    async (payload: propsPayload) => {
        const resp = await fetch(`http://localhost:7000/todos/${payload.id}`, {
            method: 'DELETE',
        });

        if (resp.ok) {
            return { id: payload.id };
        }
    }
);


export const todoSlice = createSlice({
    name: 'todos',
    initialState: initialState,
    reducers: {
        addTodo: (state: Array<propsPayload>, action: PayloadAction<propsPayload>) => {
            const todo = {
                id: nanoid(),
                title: action.payload.title,
                completed: false,
            };
            state.push(todo);
        },
        toggleComplete: (state: Array<propsPayload>, action: PayloadAction<propsPayload>) => {
            const index = state.findIndex((todo) => todo.id === action.payload.id);
            state[index].completed = action.payload.completed;
        },
        deleteTodo: (state: Array<propsPayload>, action: PayloadAction<propsPayload>) => {
            return state.filter((todo) => todo.id !== action.payload.id);
        },
    },
    extraReducers: (builder) => {
        builder
            .addCase(getTodosAsync.fulfilled, (state, action:any) => {
                return action.payload.todos
            })
            .addCase(addTodoAsync.fulfilled, (state, action:any) => {
                state.push(action.payload.todo)
            })
            .addCase(toggleCompleteAsync.fulfilled, (state, action:any) => {
                const index = state.findIndex(
                    (todo) => todo.id === action.payload.todo.id
                )
                state[index].completed = action.payload.todo.completed
            })
            .addCase(deleteTodoAsync.fulfilled, (state, action:any) => {
                return state.filter((todo) => todo.id !== action.payload.id)
            })
    }
});

export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions;

export default todoSlice.reducer;



But if I remove the 'any' from action, this occurred:

Object is possibly 'undefined'.ts(2532)
(parameter) action: PayloadAction<{
    todos: any;
} | undefined, string, {
    arg: void;
    requestId: string;
    requestStatus: "fulfilled";
}, never>
Object is possibly 'undefined'.ts(2532)
(property) payload: {
    todos: any;
} | undefined


Solution

  • Do not type the action type at all. It is inferred correctly from the type of your asyncThunk if you do nothing at all.

    
                .addCase(addTodoAsync.fulfilled, (state, action) => {
    

    Just like state is automatically the right type, action is too - the type of a fulfulled action dispatched by addTodoAsync.