Skip to content

Question: Typing thunkAPI without circular references #2237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MarioUnlam opened this issue Apr 13, 2022 · 8 comments
Closed

Question: Typing thunkAPI without circular references #2237

MarioUnlam opened this issue Apr 13, 2022 · 8 comments

Comments

@MarioUnlam
Copy link

I have this store config in two files:

export const rootReducer = combineReducers({
    auth: AuthSlice,
    form: FormSlice,
});

export const store = configureStore({
    reducer: rootReducer,
    middleware: getDefaultMiddleware =>
        getDefaultMiddleware({
            thunk: {
                extraArgument: apiService,
            },
        }),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

And it works fine. However, I want to create a type for createAsyncThunk, so I can get a typed getState and extra. I tried this:

export type AsyncThunkConfig = {
    state: RootState;
    dispatch: AppDispatch;
    extra: typeof apiService;
    rejectValue?: unknown;
    serializedErrorType?: unknown;
    pendingMeta?: unknown;
    fulfilledMeta?: unknown;
    rejectedMeta?: unknown;
};

However, as soon as I create this type, typescript starts complaining that "state" and "dispatch" are being "referenced directly or indirectly in its own type annotation". I assume this happens because I'm trying to set the return type of getState, by infering the return type of getState. Isn't there a way to get the type of my state without ReturnType?

@markerikson
Copy link
Collaborator

Can you show the rest of the setup? Where and how are you using AsyncThunkConfig? A sandbox or repo would be helpful here.

@MarioUnlam
Copy link
Author

I think that's all the relevant redux-related code. I also have these hooks in another file:

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

And this is an example slice

Code
export interface AuthState {
    isAuthenticated: boolean | null;
    isLoading: boolean;
    errorMessage: string;
}

export const initialState: AuthState = {
    isAuthenticated: null,
    isLoading: false,
    errorMessage: '',
};

export const login = createAsyncThunk<LoginUser, void, AsyncThunkConfig>(
    'post/login',
    async (argument, thunkAPI) => {
        try {
            var result = null;
            result = await thunkAPI.extra.auth.login(argument.username, argument.password);
            const data: LoginResponse = result.data;

            await setAuthData(
                '',
                data.authenticationResult.expiresIn,
                '',
                data.authenticationResult.refreshToken,
                data.authenticationResult.idToken,
            );

            return;
        } catch (err: any) {
            consoleLog('Error', err);
            if (!err.response) {
                throw err;
            }
            return thunkAPI.rejectWithValue(err.response.data);
        }
    },
);

export const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {
        
    },
    extraReducers: {
        // Login
        [login.fulfilled.type]: (state, action) => {
            state.isAuthenticated = true;
            state.isLoading = false;
            state.errorMessage = '';
        },
        [login.pending.type]: (state, action) => {
            state.isLoading = true;
            state.errorMessage = '';
        },
        [login.rejected.type]: (state, action) => {
            state.isLoading = false;
            state.isAuthenticated = false;
            state.errorMessage = action.payload?.message;
        },
    },
});

export default authSlice.reducer;

As you can see, AsyncThunkConfig is used as the third type of createAsyncThunk.

The errors I'm getting is "'state' is referenced directly or indirectly in its own type annotation" and "Type alias 'RootState' circularly references itself.", but it only happens if I use "state: RootState;" inside AsyncThunkConfig. I guess it's related to how I use typeof and ReturnType.

@markerikson
Copy link
Collaborator

One workaround might be to do type RootState = ReturnType<typeof rootReducer>, especially since you're already defining that separately.

Also, as a side note: you really should be using the "builder callback" form of extraReducers, especially since you're using TS. That will get the right type of action inferred automatically.

@MarioUnlam
Copy link
Author

MarioUnlam commented Apr 13, 2022

Thanks for the tip.

I tried using type RootState = ReturnType<typeof rootReducer>, but I get the same error. In fact, it also complains that "rootReducer" is being referenced directly or indirectly in its own initializer.

It's weird, If I only declare the store objects and types (rootReducer, store, RootState, AppDispatch, AsyncThunkConfig), it works fine. But as soon as I do this:

createAsyncThunk<any, any, AsyncThunkConfig>()

Everything breaks and typescript starts complaining. Looks like createAsyncThunk does something with this generic type, which makes the store reference itself somehow.

@markerikson
Copy link
Collaborator

Frankly that's kinda weird :( This is why I asked for a sandbox or a repo, so we can poke at it ourselves.

@MarioUnlam
Copy link
Author

I created a code sandbox with the minimum code needed to reproduce this error:

https://codesandbox.io/s/goofy-meitner-ls1hmf

CodeSandbox won't display typescript errors, but after downloading the project and testing it locally, I get the same errors.

@phryneas
Copy link
Member

@MarioUnlam if you use the notation that we recommend to use for TypeScript, the errors go away

    extraReducers: builder => {
        // -------------- //
        // Async Reducers //
        // -------------- //

        // Login
        builder.addCase(login.fulfilled, (state, action) => {
            state.isAuthenticated = true;
            state.isLoading = false;
            state.errorMessage = '';
        })
        builder.addCase(login.pending, (state, action) => {
            state.isLoading = true;
            state.errorMessage = '';
        })
        builder.addCase(login.rejected, (state, action) => {
            state.isLoading = false;
            state.isAuthenticated = false;
            state.errorMessage = action.payload?.message;
        })
    },

@MarioUnlam
Copy link
Author

Ah, nice. I was going to apply that notation anyways, but I didn't expect it to fix this issue. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants