Redux-toolkit: Why is typescript saying that the action payload is possibly undefined in the extraReducer in a slice?

Created on 18 Oct 2020  路  3Comments  路  Source: reduxjs/redux-toolkit

I created a login thunk action creator with createAsyncThunk:

export const login = createAsyncThunk<
    Tokens, // Return type of the payload creator
    Credentials, // First argument to the payload creator
    {
        dispatch: AppDispatch;
        state: RootState;
        extra: undefined;
        rejectValue: LoginError; // The type of the response I get from the server
    }
>('auth/login', async (credentials: Credentials, thunkAPI) => {
    try {
        // I've shortened it for the sake of brevity. But if this is relevant, I've included the code I've used below
        const response = await client.post<LoginResponse>('login', credentials);
        const { accessToken, refreshToken } = response.data;
        return { accessToken, refreshToken };
    } catch (e) {
        const error = e as AxiosError<LoginError>;
        const response = error.response as AxiosResponse<LoginError>;
        return thunkAPI.rejectWithValue(response.data);
    }
});

Then in the authSlice I handle the lifecycle actions:

export const authSlice = createSlice({
    name: 'auth',
    initialState,
    extraReducers: builder => {
        builder.addCase(login.pending, (state, action) => {
            state.status = 'pending';
        });
        builder.addCase(login.fulfilled, (state, action) => {
            state.status = 'loggedIn';
        });
        builder.addCase(login.rejected, (state, action) => {
            state.status = 'failed';
            state.error = action.payload; // Here the typescript compiler says: "action.payload is possibly 'undefined'". The type of state.error is LoginError | null
        });
    }
});

Why is that? I passed the rejectValue: LoginError; as the generic parameter to createAsyncThunk. action.payload will never be undefined. As soon as the login.rejected case reducer is called, shouldn't typescript know that action.payload is certainly of type LoginError?

The code I've used for the request:

const params = new URLSearchParams();
    params.append('scope', 'openid profile offline_access whee_api email');
    params.append('username', encodeURIComponent(credentials.usernameOrEmail));
    params.append('password', encodeURIComponent(credentials.password));
    params.append('client_id', 'whee_mobile');
    params.append('grant_type', 'password');
    try {
        const response = await identityClient.post<LoginResponse>('login', params);
        const { accessToken, refreshToken } = response.data;
        return { accessToken, refreshToken };
    } catch (e) {
        const error = e as AxiosError<LoginError>;
        const response = error.response as AxiosResponse<LoginError>;
        return thunkAPI.rejectWithValue(response.data);
    }

Most helpful comment

That's not the issue here.
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:

  1. you reject it with rejectWithValue - in this case, payload is set.
  2. 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.

Ahh, that makes sense. Thank you very much!

All 3 comments

I would suggest reviewing the type of response.data in your catch {} clause. It's likely that's being defined as LoginError | undefined, or something along those lines.

That's not the issue here.
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:

  1. you reject it with rejectWithValue - in this case, payload is set.
  2. 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.

That's not the issue here.
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:

  1. you reject it with rejectWithValue - in this case, payload is set.
  2. 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.

Ahh, that makes sense. Thank you very much!

Was this page helpful?
0 / 5 - 0 ratings