如何使用 TypeScript/RTK 在被拒绝的案例中定义有效负载对象的类型

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

我刚刚用 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; } & ({ ...; } | ({ ...; } & {})), 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'.

同样按照建议,我尝试将我的案例设置为

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

并从 '@reduxjs/toolkit' 导入 PayloadAction。这也不起作用并导致以下错误

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'. 

在您的操作中,将其更改为

// 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 RequestStatus {
  Pending = 'PENDING',
  Error = 'ERROR',
  Success = 'SUCCESS',
}

interface ForgotPasswordState {
  status: RequestStatus
  errorMessage: string
}

然后在你的各种减速器中,而不是设置 isPendingisErrorisSuccess 并单独调用它们,你可以将其设置为

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

您的选择器不需要更改,但订阅切片的任何内容都需要更改逻辑

  if (forgotPasswordStatus.isError) {...}

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

经过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)
      }
    }
);

在那里指定泛型将在其他任何地方处理它。

经过一些研究,我发现了这个线程,它清除了所有内容。

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

The thing is: While you know for sure that if there is a payload, it is of type LoginError, there are still two scenarios you can come to that rejected situation:

  • you reject it with rejectWithValue - in this case, payload is set.
  • an error is thrown - in this case, payload is not set. This might happen if you re-trow an error, or an error occurs in your catch block, or outside of it. As we cannot know for sure if that situation can occur in your code, we have to assume it can - and so payload is optional.