How to properly type a thunk with ThunkAction using redux-thunk in Typescript?

Viewed 6937

I'm trying to type check my redux-thunk code with Typescript.

From the official docs of Redux: Usage with Redux Thunk, we get this example:

// src/thunks.ts

import { Action } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage = (
  message: string
): ThunkAction<void, RootState, unknown, Action<string>> => async dispatch => {
  const asyncResp = await exampleAPI()
  dispatch(
    sendMessage({
      message,
      user: asyncResp,
      timestamp: new Date().getTime()
    })
  )
}

function exampleAPI() {
  return Promise.resolve('Async Chat Bot')
}

To reduce repetition, you might want to define a reusable AppThunk type once, in your store file, and then use that type whenever you write a thunk:

export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>

QUESTION

I'm not fully understanding the use of the ThunkAction type:

ThunkAction<void, RootState, unknown, Action<string>>

There are 4 type params, right?

1st - void

This is the return type of the thunk, right? Shouldn't it be Promise<void>, since it's async?

2nd - RootState

It's the full state shape, right? I mean, it's not a slice, but the full state.

3rd - unknown

Why is this unknown? What is this?

4th - Action<string>

Also didn't understand this. Why is Action<T> taking a string as a parameter? Should it always be string? Why is it?

2 Answers

From the typings at https://github.com/reduxjs/redux-thunk/blob/d28ab03fd1d2dd1de402928a9589857e97142e09/src/index.d.ts

/**
 * A "thunk" action (a callback function that can be dispatched to the Redux
 * store.)
 *
 * Also known as the "thunk inner function", when used with the typical pattern
 * of an action creator function that returns a thunk action.
 *
 * @template TReturnType The return type of the thunk's inner function
 * @template TState The redux state
 * @template TExtraThunkARg Optional extra argument passed to the inner function
 * (if specified when setting up the Thunk middleware)
 * @template TBasicAction The (non-thunk) actions that can be dispatched.
 */
export type ThunkAction<
  TReturnType,
  TState,
  TExtraThunkArg,
  TBasicAction extends Action
> = (
  dispatch: ThunkDispatch<TState, TExtraThunkArg, TBasicAction>,
  getState: () => TState,
  extraArgument: TExtraThunkArg,
) => TReturnType;

First - TReturnType

Here we don't care about the return type - we're not waiting for the result of the thunk anyway. In TS it's ok to assign any function to a void signature, as it's not going to hurt type safety. Here's an example showing that I can assign various async/non-async functions to a void function signature:

playground void assignment screenshot

We use the provided dispatch function to trigger the next action we care about - so the only thing that matters in terms of timing is that internally to the async function we are awaiting certain things. Redux-thunk internally is not doing anything with the result of this function. Here's a great explainer on how thunks work under the hood (just the preview is necessary):

https://frontendmasters.com/courses/rethinking-async-js/synchronous-and-asynchronous-thunks/

Second - TState Yep - this is the whole store state type

Third - TExtraThunkARg You can add your own custom extra argument that gets passed into the thunk after dispatch and getState. This defaults to unknown, as unless you explicitly provide it then it's not clear what it will be. This is type safe, as trying to interact with an unknown argument will lead to compile time errors.

More here: https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument

Fourth - TBasicAction

This is an action type. Action is the most basic type of action - where every type property is a plain string. Optionally you can provide your own more specific types - i.e type MyActionType = 'FOO_ACTION' | 'BAR_ACTION' for further type safety/narrowing. You would then use this as Action.

Actually, most of the answers can be found in the source code.

 * @template TReturnType The return type of the thunk's inner function
 * @template TState The redux state
 * @template TExtraThunkARg Optional extra argument passed to the inner function
 * (if specified when setting up the Thunk middleware)
 * @template TBasicAction The (non-thunk) actions that can be dispatched.
 */
export type ThunkAction<
  TReturnType,
  TState,
  TExtraThunkArg,
  TBasicAction extends Action
> = (
  dispatch: ThunkDispatch<TState, TExtraThunkArg, TBasicAction>,
  getState: () => TState,
  extraArgument: TExtraThunkArg,
) => TReturnType;

1st - void

The return type of the thunk is NOT a Promise. It is just a function that returns an inner function. Since the inner function it does not return anything, the return type is void.

2nd - RootState

Yes that is correct. Refer to the above source code. Specifically, this line * @template TState The redux state.

3rd - unknown

You read about unknown here.

4th - Action<string>

Refer to this line * @template TBasicAction The (non-thunk) actions that can be dispatched.

 * @template T the type of the action's `type` tag.
 */
export interface Action<T = any> {
  type: T
}

Code snippet is extracted from here.

Action<T> accepts string type because the action type IS a string, and the ThunkAction type is expecting an Action type as seen here - TBasicAction extends Action.

CORRECTION

1st - void

I had misunderstood and forgotten about async functions. See the first comment below.

Related