Как определить тип объекта полезной нагрузки внутри отклоненного обращения с помощью TypeScript / RTK

#reactjs #typescript #redux-toolkit

Вопрос:

Я только что создал свое первое приложение с RTK. Все работало нормально, поэтому я подумал, что брошу вызов самому себе и конвертирую его в TypeScript. Преобразовав свой первый компонент, я избавился от всех ошибок, кроме одной. Я думаю, я изо всех сил пытаюсь понять, где определить тип.

В моем extraReducers случае у меня есть два отклоненных обращения из двух асинхронных действий. Ошибка, которую я получаю, довольно проста, Object is of type 'unknown' в моих payload свойствах внутри отклоненных обращений. Но я не могу понять, где определить объект полезной нагрузки. Любая помощь приветствуется. Код ниже

 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;
 

Что я пробовал


Как и было предложено, я попытался установить в моем случае

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

И создание (я думаю) соответствующего типа

 export interface ErrorPayload {
  message: string
}
 

но это просто приводит к

 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; } amp; ({ ...; } | ({ ...; } amp; {})), SerializedError>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } amp; ({ rejectedWithValue: true; } | ({ ...; } amp; {})), 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'.
 

Также, как и предлагалось, я попытался установить для своего обращения значение

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

и импорт PayloadAction из '@reduxjs/toolkit' . Это также не сработало и привело к следующим ошибкам

 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; } amp; ({ ...; } | ({ ...; } amp; {})), SerializedError>>'.
      Types of parameters 'action' and 'action' are incompatible.
        Type 'PayloadAction<unknown, string, { arg: ForgotPasswordRequestProps; requestId: string; requestStatus: "rejected"; aborted: boolean; condition: boolean; } amp; ({ rejectedWithValue: true; } | ({ ...; } amp; {})), 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'. 
 

Комментарии:

1. Вы можете просто добавить аннотацию типа (state, action ) => , например: (state, action: { payload: PayloadType }) =>

2. @Cerbrus Я пробовал это, но, должно быть, я делаю что-то не так. Если я сделаю так, как вы предложили, и создам что-то вроде export interface PayloadType { message: string } , я получу ...is not assignable to parameter of type... сообщение об ошибке

Ответ №1:

В вашем действии измените его на

 // 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
 

Кроме того, чтобы уменьшить объем данных, вы можете создать enum

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

interface ForgotPasswordState {
  status: RequestStatus
  errorMessage: string
}
 

Затем в ваших различных редукторах вместо установки isPending , isError , и isSuccess и вызова их по отдельности, вы можете просто установить его как

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

Ваш селектор не нужно менять, но все, что подписано на фрагмент, должно было бы изменить логику с

   if (forgotPasswordStatus.isError) {...}

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

Комментарии:

1. спасибо за ответ. К сожалению, это тоже не сработало. Я обновил свой вопрос ответом об ошибке

Ответ №2:

Переход к использованию с TypeScript docs для 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)
      }
    }
);
 

Указание этого общего там позаботится об этом везде.

Ответ №3:

После некоторых исследований я обнаружил этот поток, который все очищает.

https://github.com/reduxjs/redux-toolkit/issues/766#issuecomment-711415387

Дело в том, что, хотя вы точно знаете, что если есть полезная нагрузка, она имеет тип LoginError, есть еще два сценария, в которых вы можете столкнуться с этой отклоненной ситуацией:

  • вы отклоняете его с помощью rejectWithValue — в этом случае устанавливается полезная нагрузка.
  • выдается ошибка — в этом случае полезная нагрузка не установлена. Это может произойти, если вы повторно создаете ошибку, или ошибка возникает в вашем блоке catch или за его пределами. Поскольку мы не можем точно знать, может ли такая ситуация возникнуть в вашем коде, мы должны предположить, что это возможно, и поэтому полезная нагрузка необязательна.