29

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?

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230

6 Answers6

35

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.

João Baraky
  • 641
  • 5
  • 11
  • 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". –  Dec 20 '20 at 02:18
  • Thanks for including the entire function for a holistic example. – Hari Reddy Jun 02 '23 at 17:24
26

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.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
14

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

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
  • 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 etc – Pumuckelo Jul 21 '21 at 06:28
  • 1
    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. – lucaslt89 Mar 02 '22 at 13:37
  • 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? – Y. Gherbi Apr 10 '23 at 11:00
  • Oh i was importing the things I used here INSIDE the declare module block. my bad! – Y. Gherbi Apr 10 '23 at 11:01
  • Ok, I don't know why this is, but getState still returns unknown. – Y. Gherbi Apr 10 '23 at 12:19
7

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

thSoft
  • 21,755
  • 5
  • 88
  • 103
  • 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. –  Dec 09 '20 at 00:00
4

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>;
}
Ahmed Hamed
  • 181
  • 1
  • 8
4

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;
  }
);
Evaldas B
  • 2,464
  • 3
  • 17
  • 25