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

2

10

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?

Inna answered 14/9, 2020 at 9:15 Comment(0)
P
10

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.

Phlegethon answered 14/9, 2020 at 10:52 Comment(0)
M
4

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.

Milagrosmilam answered 14/9, 2020 at 10:44 Comment(7)
I believe the explanation of point #1 is not quite right. Async functions do return promises by default - and you can see TS infer this in this example: typescriptlang.org/play?#code/…Phlegethon
Thanks for your answer. One last thing about the 3rd item. I wasn't asking about what is the unknown type itself, but rather whay am I typing as unkown in the 3rd parameter? I've gone deeper into the source code and I think it's the type for the extra argument, when you initialize the thunk middleware as applyMiddleware(thunk.withExtraArgument(api)), for example. So you'll get return (dispatch, getState, api) in your thunks' "creators". It this is correct, you could add that to your answer. Thanks again.Inna
Rather - it's acceptable within TS to assign any function to a function type with return type void: typescriptlang.org/play?#code/…Phlegethon
@Phlegethon I agree with you. It's the return type for the actual thunk, which usually is async and not the thunk "creator". I think it's not mandatory that it should be async, is it? So, could we use Promise<void> | void ?Inna
@Inna - yes you're right in both cases. It's for the thunk - not the thunk creator, and it's not mandatory to be async. Promise<void> would be a more accurate type here - but really it's just extra unnecessary complexity. Unless another part of your code requires the result of the function to be more specific (for example if redux-thunk internally cared about the fact that the function is a promise) - then there's no need to add the extra information.Phlegethon
@Phlegethon you are spot on. I had forgotten all about async functions in redux-thunk. Thanks for pointing it out ;)Milagrosmilam
@Inna unknown is used for this API to signal “this can be any value, so you must perform some type of checking before you use it”. This forces users to safely introspect returned values.Milagrosmilam

© 2022 - 2024 — McMap. All rights reserved.