question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Typescript] Unable to create a slice creator with thunk

See original GitHub issue

I’m trying to build a function that return a slice and thunk, which will use generic type, here’s a minimal code snippet:

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface _State {
  status: 'NOT_LOADED' | 'LOADING' | 'LOADED' | 'ERROR';
  error?: string;
}

const createNrSlice = <Parameter extends object>() => {
  const startFetch = (
    state: _State,
    action: PayloadAction<Parameter>
  ) => {
    console.log(action)
  }

  const fetchFailed = (
    state: _State,
    action: PayloadAction<string>
  ) => {
    console.log(action)
  }

  const initialState: _State = {
    status: 'NOT_LOADED',
  };

  const slice = createSlice({
    name: 'testSlice', 
    initialState,
    reducers: {
      start: startFetch,
      failed: fetchFailed,
    }
  })

  slice.actions.start({} as Parameter) // ERROR
  slice.actions.failed('') // PASS
}

/*
ERROR MESSAGE:
~~~~~~~~~~~~~~~
This expression is not callable.
  Each member of the union type 'ActionCreatorWithPayload<any, string> | ActionCreatorWithNonInferrablePayload<string> | ActionCreatorWithoutPayload<string> | ActionCreatorWithOptionalPayload<...> | ActionCreatorWithPayload<...>' has signatures, but none of those signatures are compatible with each other.ts(2349)
*/

slice.action.start is expected to be something like ActionCreatorWithPayload<Parameter>(payload: Parameter) but I just got an error.

It’s okay if i call slice.actions.start outside the createNrSlice function, but if I call it inside the function, there will be an error.

The problem is if I want to make a function return both a slice and a thunk, I must call actions inside the function.

We’ve discussed a lot about this problem in the Redux discord channel but got no result, and would like to know if it’s a bug related to the TypeScript definition.


Note: Remove extends object near <Parameter extends object>() won’t solve the problem.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:14 (7 by maintainers)

github_iconTop GitHub Comments

5reactions
Lossescommented, Jan 13, 2020

@phryneas Sorry, but another question about TypeScript…

I modified a little from the official example, trying to make reducer an optional parameter, and the generic type break again:

(diff attached)

import { 
  createSlice, 
  PayloadAction,
  SliceCaseReducers,
  ValidateSliceCaseReducers 
} from '@reduxjs/toolkit'

interface GenericState<T> {
  data?: T
  status: 'loading' | 'finished' | 'error'
}

type Obj = Record<string, any>

const createGenericSlice = <
  D extends Obj,
  T extends Obj,
  Reducers extends SliceCaseReducers<GenericState<T>>
>({
  name = '',
  initialState,
  reducers
}: {
  name: string
  initialState: GenericState<T>
  reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers> // MARK 1
  // ALTERNATIVE WAY
  // reducers?: ValidateSliceCaseReducers<GenericState<T>, Reducers> //MARK 2
}) => {
  return createSlice({
    name,
    initialState,
    reducers: {
      start: {
        reducer(state: GenericState<T>, action: PayloadAction<D>) {
          state.status = 'loading'
          console.log(action)
        },
        prepare(payload: D){return {payload}},
      },
      success(state: GenericState<T>, action: PayloadAction<T>) {
        state.data = action.payload
        state.status = 'finished'
      },
      ...reducers
    }
  })
}
const wrappedSlice = createGenericSlice({
  name: 'test',
  initialState: { status: 'loading' } as GenericState<Obj>,
  reducers: {
    magic(state) {
      state.status = 'finished'
      state.data = {}
    }
  }
})

const actions = wrappedSlice.actions

type Actions = keyof typeof actions

// THE RESULT OF MARK 1:
// type Actions = "magic" | "start" | "success"
// THE RESULT OF MARK 2:
// type Actions = "start" | "success"

// Why "magic" disappeared?

My question is: Why turn the type of additional reducers to optional will change the type of actions

While using wrapped createSlice, users must always provide a reducer even if it’s not necessary?

Diff

+import { 
+  createSlice, 
+  PayloadAction,
+  SliceCaseReducers,
+  ValidateSliceCaseReducers 
+} from '@reduxjs/toolkit'
+
 interface GenericState<T> {
   data?: T
   status: 'loading' | 'finished' | 'error'
 }
+
+type Obj = Record<string, any>
+
 const createGenericSlice = <
-  T,
+  D extends Obj,
+  T extends Obj,
   Reducers extends SliceCaseReducers<GenericState<T>>
 >({
   name = '',
 const createGenericSlice = <
 }: {
   name: string
   initialState: GenericState<T>
-  reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
+  reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers> // MARK 1
+  // ALTERNATIVE WAY
+  // reducers?: ValidateSliceCaseReducers<GenericState<T>, Reducers> //MARK 2
 }) => {
   return createSlice({
     name,
     initialState,
     reducers: {
-      start(state) {
-        state.status = 'loading'
+      start: {
+        reducer(state: GenericState<T>, action: PayloadAction<D>) {
+          state.status = 'loading'
+          console.log(action)
+        },
+        prepare(payload: D){return {payload}},
       },
       success(state: GenericState<T>, action: PayloadAction<T>) {
         state.data = action.payload
         state.status = 'finished'
  const createGenericSlice = <
 }
 const wrappedSlice = createGenericSlice({
   name: 'test',
-  initialState: { status: 'loading' } as GenericState<string>,
+  initialState: { status: 'loading' } as GenericState<Obj>,
   reducers: {
     magic(state) {
       state.status = 'finished'
-      state.data = 'hocus pocus'
+      state.data = {}
     }
   }
 })
+
+const actions = wrappedSlice.actions
+
+type Actions = keyof typeof actions
+
+// THE RESULT OF MARK 1:
+// type Actions = "magic" | "start" | "success"
+// THE RESULT OF MARK 2:
+// type Actions = "start" | "success"
+
+// Why "magic" disappeared?

When I'm Debugging

2reactions
Lossescommented, Jan 15, 2020

Thanks for your help! But you know… It looks so dirty… hope TypeScript could merge the PR mentioned on your twitter as soon as possible…


Update:

The code that works would looks like this:

import { 
  createSlice, 
  PayloadAction,
  SliceCaseReducers,
  ValidateSliceCaseReducers
} from '@reduxjs/toolkit'

interface GenericState<T> {
  data?: T
  status: 'loading' | 'finished' | 'error'
}

type Obj = Record<string, any>

const createGenericSlice = <
  D extends Obj,
  T extends Obj
>({
  name = '',
  initialState,
  
}: {
  name: string
  initialState: GenericState<T>
}) => {
  return <Reducers extends SliceCaseReducers<GenericState<T>>>(
    reducers: Reducers & ValidateSliceCaseReducers<GenericState<T>, Reducers> = {} as Reducers & ValidateSliceCaseReducers<GenericState<T>, Reducers>) => {
    return createSlice({
      name,
      initialState,
      reducers: {
        start: {
          reducer(state: GenericState<T>, action: PayloadAction<D>) {
            state.status = 'loading'
            console.log(action)
          },
          prepare(payload: D){return {payload}},
        },
        success(state: GenericState<T>, action: PayloadAction<T>) {
          state.data = action.payload
          state.status = 'finished'
        },
        ...reducers
      }
    })
  }
}

const wrappedSlice = createGenericSlice({
  name: 'test',
  initialState: { status: 'loading' } as GenericState<Obj>
})({
  magic(state) {
    state.status = 'finished'
    state.data = {}
  }
})

const actions = wrappedSlice.actions

type Actions = keyof typeof actions

Really complex anyway…

Read more comments on GitHub >

github_iconTop Results From Across the Web

Usage With TypeScript - Redux Toolkit
As TS cannot combine two string literals ( slice.name and the key of actionMap ) into a new literal, all actionCreators created by...
Read more >
Redux-Toolkit with Typescript. Cannot dispatch action created ...
Here is my implementation which is done according to the official Redux-Toolkit documentation. user-slice.ts: export interface User { UserId: ...
Read more >
How to Use Thunks with Redux Toolkit and TypeScript
Let's change our app a bit and, as an example, create a method to load todos from the server and show them. Refactoring#....
Read more >
Handling Errors in Async Thunks with builder.addCase()
When you're handling a thunk's rejected state with builder.addCase TypeScript is able to infer that your action includes an extra error ...
Read more >
Using TypeScript with Redux Toolkit - LogRocket Blog
With the createSlice API, we are able to easily set up the store with just a few lines of code. The useSelector Hook...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found