Skip to content

Why is typescript saying that the action payload is possibly undefined in the extraReducer in a slice? #766

Closed

Description

@Kleinhummel

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

Activity

  1. markerikson commented on Oct 18, 2020

    @markerikson
    Collaborator

    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.

  2. phryneas commented on Oct 18, 2020

    @phryneas
    Member

    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.
  3. Kleinhummel commented on Oct 18, 2020

    @Kleinhummel
    Author

    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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions