Cannot set getState type to RootState in createAsyncThunk
Asked Answered
P

6

31

I cannot set the return type of getState() to RootState. I'm using typescript and VSCode. I have to set the type to any, which stops IntelliSense on that object. Below is the code that has the problem:

export const unsubscribeMeta = createAsyncThunk(
  'meta/unsubscribe',
  async (_, { getState }) => {
    const { meta } = getState() as any;
    const res = await client.post<apiUnsubscribeResponse>(
      `/meta/unsubscribe/${meta.subscriptionId}`
    );
    return res.data.data;
  }
);

If I try to use RootState instead of any, many errors are flagged in the module by VSCode. I believe it is due to a circular dependency with the store and this slice. I am using RootState in many places further down in the module for selectors, with no problem. Is there a way around this?

Pyles answered 11/11, 2020 at 20:13 Comment(0)
S
28

You don't really need to know about the shape of the entire state. You just need to know about the presence of the values which you are trying to access.

If you can access the whole state.meta type:

const { meta } = getState() as { meta: MetaState };

If not:

const { meta } = getState() as { meta: { subscriptionId: string } };

I recommend this sort of approach for avoiding the circular dependency because the root state will always depend on the slices, so the slices should not depend on the root.

Saladin answered 12/11, 2020 at 6:45 Comment(2)
Perfect. Thank you.Pyles
This works if you don't need to pass the whole store to a selector. Otherwise you need to use the root state.Foreglimpse
M
38

The createAsyncThunk can have the types defined on the generics:

export const unsubscribeMeta = createAsyncThunk<apiUnsubscribeResponse, void, {state: RootState }>(
  'meta/unsubscribe',
  async (_, { getState }) => {
    const { meta } = getState();
    const res = await client.post<apiUnsubscribeResponse>(
      `/meta/unsubscribe/${meta.subscriptionId}`
    );
    return res.data.data;
  }
);

Defining the state will automatically make the getState be aware of the application state.

Menstrual answered 18/12, 2020 at 20:32 Comment(2)
Thank you for your answer. While this does work, I still prefer the answer given by Linda Paiste. Declaring the slice state explicitly eliminates the use of "any". My typescript/eslint rules discourage the explicit use of "any".Pyles
Thanks for including the entire function for a holistic example.Allister
S
28

You don't really need to know about the shape of the entire state. You just need to know about the presence of the values which you are trying to access.

If you can access the whole state.meta type:

const { meta } = getState() as { meta: MetaState };

If not:

const { meta } = getState() as { meta: { subscriptionId: string } };

I recommend this sort of approach for avoiding the circular dependency because the root state will always depend on the slices, so the slices should not depend on the root.

Saladin answered 12/11, 2020 at 6:45 Comment(2)
Perfect. Thank you.Pyles
This works if you don't need to pass the whole store to a selector. Otherwise you need to use the root state.Foreglimpse
M
16

You can use Typescript's module augmentation feature to assign the default state to AsyncThunkConfig.state which will be the returned type of getState() when we call it later on.

declare module "@reduxjs/toolkit" {
  type AsyncThunkConfig = {
    state?: unknown;
    dispatch?: Dispatch;
    extra?: unknown;
    rejectValue?: unknown;
    serializedErrorType?: unknown;
  };

  function createAsyncThunk<
    Returned,
    ThunkArg = void,
    ThunkApiConfig extends AsyncThunkConfig = {
      state: YourRootState; // this line makes a difference
    }
  >(
    typePrefix: string,
    payloadCreator: AsyncThunkPayloadCreator<
      Returned,
      ThunkArg,
      ThunkApiConfig
    >,
    options?: any
  ): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;
}

Where YourRootState is the type of your store state.

type YourRootState = {
  myNumber: number;
  myString: string;
};

Now you can use createAsyncThunk as usual and getState() returns the correct type.

const doSomethingAsync = createAsyncThunk(
  "mySlice/action",
  async (_, { getState, dispatch }) => {
    const rootState = getState(); // has type YourRootState

    console.log(rootState.myNumber);
    console.log(rootState.myString);
  }
);


function Child() {
  const dispatch = useDispatch();
  return <button onClick={() => dispatch(doSomethingAsync())}>Click</button>;
}

Live Demo

Edit 64793504/cannot-set-getstate-type-to-rootstate-in-createasyncthunk

Myron answered 26/2, 2021 at 8:47 Comment(5)
thank you, this works great. If you dont know where to paste this code snippet. Just paste it into the file where you declared your root state etcStearoptene
This should be the accepted answer as it doesn't require casting or knowing the shape of the state. I personally don't like casting or having to know the shape of an object, since that's the whole point of Typescript (let the language infer it). This also avoids circular dependencies.Already
For some reason it looses all the typings from @reduxjs/toolkit that are not createAsyncThunk. I want to add this, not to overwrite everything in the module. Am I missing a config setting maybe?Maternal
Oh i was importing the things I used here INSIDE the declare module block. my bad!Maternal
Ok, I don't know why this is, but getState still returns unknown.Maternal
M
8

The best way is to create a pretyped async thunk somewhere in a utility file as documentation suggests:

export const createAppAsyncThunk = createAsyncThunk.withTypes<{
  state: RootState
  dispatch: AppDispatch
  rejectValue: string
  // extra: { s: string; n: number } // This is extra data prop, can leave it out if you are not passing extra data
}>()

After doing this you will have all of your types automatically in all thunks that you create using createAppAsyncThunk

So in OP's case it would look like this:

export const unsubscribeMeta = createAppAsyncThunk (
  'meta/unsubscribe',
  async (_, { getState }) => {
    const { meta } = getState() // This is typed automatically 🎉
    const res = await client.post<apiUnsubscribeResponse>(
      `/meta/unsubscribe/${meta.subscriptionId}`
    );
    return res.data.data;
  }
);
Manzanares answered 20/12, 2022 at 20:55 Comment(1)
I wasn't aware that this exists. This answer helped me a lot. Thanks!Toft
M
7

Simply omit state: RootState from your ThunkApiConfig type, then you can use const state = getState() as RootState; in your payloadCreator without circular dependency.

Murry answered 7/12, 2020 at 10:41 Comment(1)
I'm not a Redux expert by any means. I've only been using Redux for a couple of months, and I only use the toolkit. I cannot find any documentation related to ThunkApiConfig in the Redux toolkit documentation. However, I probably would stick with the solution provided by Linda Paiste. I like explicitly declaring the slice state rather than having it implied by the initial values.Pyles
H
5

I will continue on NearHuscarl answer since I can't suggest an edit to it.

NearHuscarl answer is great but the problem with it that he set the options type to any, so it solves a problem it raises another since now if you use options in createAsyncThunk you have to set all of its types manually or typescript will raise Binding element implicitly has an 'any' type. error.

So simply setting options type like below would solve that problem.

declare module "@reduxjs/toolkit" {
    type AsyncThunkConfig = {
        state?: unknown;
        dispatch?: Dispatch;
        extra?: unknown;
        rejectValue?: unknown;
        serializedErrorType?: unknown;
    };

    function createAsyncThunk<
        Returned,
        ThunkArg = void,
        ThunkApiConfig extends AsyncThunkConfig = { state: RootState } // here is the magic line
    >(
        typePrefix: string,
        payloadCreator: AsyncThunkPayloadCreator<
            Returned,
            ThunkArg,
            ThunkApiConfig
        >,
        options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>,
    ): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;
}
Helms answered 3/9, 2021 at 1:8 Comment(1)
I don't understand why TS still thinks getState returns unknown... Am I missing something?Maternal

© 2022 - 2024 — McMap. All rights reserved.