Home > Mobile >  How to define type of payload object inside rejected case using TypeScript/RTK
How to define type of payload object inside rejected case using TypeScript/RTK

Time:10-28

I've just built my first app with RTK. Everything was working fine so I thought I would challenge myself and convert it to TypeScript. Converting my first component, I've gotten rid of all the errors except one. I guess I'm struggling to work out where to define the type.

In my extraReducers, I have two rejected cases from two async actions. The error I am getting is pretty simple, Object is of type 'unknown' on my payload properties inside the rejected cases. But I can't work out where to define the payload Object. Any help is greatly appreciated. Code below

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'rootReducer';
import Axios from 'utils/axios';

interface ForgotPasswordState {
  status: {
    isPending: boolean;
    isSuccess: boolean;
    isError: boolean;
    errorMessage: string;
  },
}

export interface ForgotPasswordRequestProps {
  email: string;
}

export interface ForgotPasswordSetProps {
  query: string | null;
  password: string
}

export const forgotPasswordRequest = createAsyncThunk(
  'auth/forgotPasswordRequest',
  async (
    data: ForgotPasswordRequestProps, 
    { rejectWithValue }) => {
      const { email } = data;
      try {
        const response = await Axios.anon.post(`/v1/auth/forgot-password`, {
          email,
        });
  
        if (response.status === 204) {
          return null;
        }
      } catch (error) {
        if (error instanceof Error) {
          return rejectWithValue(error.message);
        }
        return console.log('ERROR', error);
      }
    }
);

export const forgotPasswordSet = createAsyncThunk(
  'auth/verifyEmailSet',
  async (
    data: ForgotPasswordSetProps,
    { rejectWithValue }) => {
      const { query, password } = data;
      try {
        const response = await Axios.anon.post(`/v1/auth/reset-password?token=${query}`, {
          password,
        });
    
        if (response.status === 204) {
          return null;
        }
      } catch (error) {
        if (error instanceof Error) {
          return rejectWithValue(error.message)
        }
        return console.log("ERROR", error)
      }
    }
);

const initialState: ForgotPasswordState = {
  status: {
    isPending: false,
    isSuccess: false,
    isError: false,
    errorMessage: '',
  },
}

export const forgotPasswordSlice = createSlice({
  name: 'forgotPassword',
  initialState,
  reducers: {
    clearState: (state) => {
      state.status.isError = false;
      state.status.isSuccess = false;
      state.status.isPending = false;
      state.status.errorMessage = '';
      return state;
    },
  },
  extraReducers: builder => {
    builder
      .addCase(
        forgotPasswordRequest.fulfilled, (state) => {
          state.status.isPending = false;
          state.status.isSuccess = true;
        })
      .addCase(
        forgotPasswordRequest.rejected, (state, action ) => {
          const { payload } = action;
          state.status.isPending = false;
          state.status.isError = true;
          // "Object is of type 'unknown'" on payload below
          state.status.errorMessage = payload.message;
        })
      .addCase(
        forgotPasswordRequest.pending, (state) => {
          state.status.isPending = true;
        })
      .addCase(
        forgotPasswordSet.fulfilled, (state) => {
          state.status.isPending = false;
          state.status.isSuccess = true;
        })
      .addCase(
        forgotPasswordSet.rejected, (state, action ) => {
          const { payload } = action;
          state.status.isPending = false;
          state.status.isError = true;
          // "Object is of type 'unknown'" on payload below
          state.status.errorMessage = payload.message;
        })
      .addCase(
        forgotPasswordSet.pending, (state) => {
          state.status.isPending = true;
        })
    }
});

export const { clearState } = forgotPasswordSlice.actions;

export const statusSelector = (state: RootState) => state.auth.forgotPassword.status;

What I've tried


As suggested, I've tried setting my case to

(state, action: { payload: ErrorPayload }) => {...}

And creating (I think) an appropriate type

export interface ErrorPayload {
  message: string
}

but this just results in

No overload matches this call.
  Overload 1 of 2, '(actionCreator: AsyncThunkRejectedActionCreator<ForgotPasswordRequestProps, {}>, reducer: CaseReducer<ForgotPasswordState, PayloadAction<...>>): ActionReducerMapBuilder<...>', gave the following error.
    Argument of type '(state: WritableDraft<ForgotPasswordState>, action: { payload: ErrorPayload; }) => void' is not assignable to parameter of type 'CaseReducer<ForgotPasswordState, PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } & ({ ...; } | ({ ...; } & {})), SerializedError>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } & ({ rejectedWithValue: true; } | ({ ...; } & {})), SerializedError>' is not assignable to type '{ payload: ErrorPayload; }'.
          Types of property 'payload' are incompatible.
            Type 'unknown' is not assignable to type 'ErrorPayload'.
  Overload 2 of 2, '(type: string, reducer: CaseReducer<ForgotPasswordState, Action<string>>): ActionReducerMapBuilder<ForgotPasswordState>', gave the following error.
    Argument of type 'AsyncThunkRejectedActionCreator<ForgotPasswordRequestProps, {}>' is not assignable to parameter of type 'string'.

Also as suggested, I've tried setting my case to

(state, action: PayloadAction<string>) => {...}

and importing PayloadAction from '@reduxjs/toolkit'. This is also didn't work and resulted in the following errors

No overload matches this call.
  Overload 1 of 2, '(actionCreator: AsyncThunkRejectedActionCreator<ForgotPasswordRequestProps, {}>, reducer: CaseReducer<ForgotPasswordState, PayloadAction<...>>): ActionReducerMapBuilder<...>', gave the following error.
    Argument of type '(state: WritableDraft<ForgotPasswordState>, action: { payload: string; type: string; }) => void' is not assignable to parameter of type 'CaseReducer<ForgotPasswordState, PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } & ({ ...; } | ({ ...; } & {})), SerializedError>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } & ({ rejectedWithValue: true; } | ({ ...; } & {})), SerializedError>' is not assignable to type '{ payload: string; type: string; }'.
          Types of property 'payload' are incompatible.
            Type 'unknown' is not assignable to type 'string'.
  Overload 2 of 2, '(type: string, reducer: CaseReducer<ForgotPasswordState, { payload: string; type: string; }>): ActionReducerMapBuilder<ForgotPasswordState>', gave the following error.
    Argument of type 'AsyncThunkRejectedActionCreator<ForgotPasswordRequestProps, {}>' is not assignable to parameter of type 'string'. 

CodePudding user response:

In your action, change it to

// Assuming that your expected action is only a message string
// If it's more than that, then change the PayloadAction type param to your expected interface
// You'll need to import PayloadAction from '@reduxjs/toolkit'
.addCasePasswordRequest.rejected, (state, action: PayloadAction<string>) => {
  state.status.isPending = false
  state.status.isError = true
  state.status.errorMessage = action.payload

Also, in order to reduce the amount of data, you may want to create an enum

enum RequestStatus {
  Pending = 'PENDING',
  Error = 'ERROR',
  Success = 'SUCCESS',
}

interface ForgotPasswordState {
  status: RequestStatus
  errorMessage: string
}

Then in your various reducers, instead of setting isPending, isError, and isSuccess and calling those individually, you can just set it as

state.status = RequestStatus.Error
state.errorMessage = action.payload

Your selector doesn't need to change but anything subscribed to the slice would need to change logic from

  if (forgotPasswordStatus.isError) {...}

  // Change to
  if (forgotPasswordStatus.status === RequestStatus.Error) {...}

CodePudding user response:

Going by the Usage with TypeScript docs for createAsyncThunk

const forgotPasswordRequest = createAsyncThunk<
  // Return type of the payload creator
  unknown,
  // First argument to the payload creator
  ForgotPasswordSetProps,
  {
    // Optional fields for defining thunkApi field types
    rejectValue: { message: string }
  }
>('auth/verifyEmailSet',
  async (
    data,
    { rejectWithValue }) => {
      const { query, password } = data;
      try {
        const response = await Axios.anon.post(`/v1/auth/reset-password?token=${query}`, {
          password,
        });
    
        if (response.status === 204) {
          return null;
        }
      } catch (error) {
        if (error instanceof Error) {
          return rejectWithValue(error.message)
        }
        return console.log("ERROR", error)
      }
    }
);

Specifying that generic there will take care of it everywhere else.

  • Related