Skip to content

Commit

Permalink
Rework types to better support getting *full* dispatch type
Browse files Browse the repository at this point in the history
  • Loading branch information
EskiMojo14 committed Jun 29, 2024
1 parent c403cf7 commit 69d0c0a
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 33 deletions.
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Action, AnyAction } from 'redux'

import type { ThunkMiddleware } from './types'

export type {
Expand Down
66 changes: 38 additions & 28 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import type { Action, AnyAction, Middleware } from 'redux'
import type { Dispatch, Middleware, Action, UnknownAction } from 'redux'

/**
* The dispatch overload provided by React-Thunk; allows you to dispatch:
* - thunk actions: `dispatch()` returns the thunk's return value
*
* @template State The redux state
* @template ExtraThunkArg The extra argument passed to the inner function of
* thunks (if specified when setting up the Thunk middleware)
*/
export interface ThunkOverload<State, ExtraThunkArg> {
<ReturnType>(
thunkAction: ThunkAction<this, State, ExtraThunkArg, ReturnType>

Check failure on line 13 in src/types.ts

View workflow job for this annotation

GitHub Actions / Test Types with TypeScript 4.7

Type 'this' does not satisfy the constraint 'ThunkDispatch<State, ExtraThunkArg, any>'.
): ReturnType
}

/**
* The dispatch method as modified by React-Thunk; overloaded so that you can
Expand All @@ -11,30 +25,26 @@ import type { Action, AnyAction, Middleware } from 'redux'
* thunks (if specified when setting up the Thunk middleware)
* @template BasicAction The (non-thunk) actions that can be dispatched.
*/
export interface ThunkDispatch<
export type ThunkDispatch<
State,
ExtraThunkArg,
BasicAction extends Action
> {
// When the thunk middleware is added, `store.dispatch` now has three overloads (NOTE: the order here matters for correct behavior and is very fragile - do not reorder these!):

// 1) The specific thunk function overload
/** Accepts a thunk function, runs it, and returns whatever the thunk itself returns */
<ReturnType>(
thunkAction: ThunkAction<ReturnType, State, ExtraThunkArg, BasicAction>
): ReturnType

// 2) The base overload.
/** Accepts a standard action object, and returns that action object */
<Action extends BasicAction>(action: Action): Action

// 3) A union of the other two overloads. This overload exists to work around a problem
// with TS inference ( see https://github.com/microsoft/TypeScript/issues/14107 )
/** A union of the other two overloads for TS inference purposes */
<ReturnType, Action extends BasicAction>(
action: Action | ThunkAction<ReturnType, State, ExtraThunkArg, BasicAction>
): Action | ReturnType
}
> = ThunkOverload<State, ExtraThunkArg> &
Dispatch<BasicAction> &
// order matters here, this must be the last overload
// this supports #248, allowing ThunkDispatch to be given a union type
// this does *not* apply to the inferred store type.
// doing so would break any following middleware's ability to match their overloads correctly
(<ReturnType, Action extends BasicAction>(
action:
| Action
| ThunkAction<
ThunkDispatch<State, ExtraThunkArg, BasicAction>,
State,
ExtraThunkArg,
BasicAction
>
) => Action | ReturnType)

/**
* A "thunk" action (a callback function that can be dispatched to the Redux
Expand All @@ -43,19 +53,19 @@ export interface ThunkDispatch<
* Also known as the "thunk inner function", when used with the typical pattern
* of an action creator function that returns a thunk action.
*
* @template Dispatch The `dispatch` method from the store
* @template ReturnType The return type of the thunk's inner function
* @template State The redux state
* @template ExtraThunkArg Optional extra argument passed to the inner function
* (if specified when setting up the Thunk middleware)
* @template BasicAction The (non-thunk) actions that can be dispatched.
*/
export type ThunkAction<
ReturnType,
Dispatch extends ThunkDispatch<State, ExtraThunkArg, any>,
State,
ExtraThunkArg,
BasicAction extends Action
ReturnType
> = (
dispatch: ThunkDispatch<State, ExtraThunkArg, BasicAction>,
dispatch: Dispatch,
getState: () => State,
extraArgument: ExtraThunkArg
) => ReturnType
Expand All @@ -82,10 +92,10 @@ export type ThunkActionDispatch<
*/
export type ThunkMiddleware<
State = any,
BasicAction extends Action = AnyAction,
BasicAction extends Action = UnknownAction,
ExtraThunkArg = undefined
> = Middleware<
ThunkDispatch<State, ExtraThunkArg, BasicAction>,
ThunkOverload<State, ExtraThunkArg>,
State,
ThunkDispatch<State, ExtraThunkArg, BasicAction>
>
2 changes: 2 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ describe('thunk middleware', () => {
const doDispatch = () => {}
const doGetState = () => 42
const nextHandler = thunkMiddleware({
// @ts-ignore
dispatch: doDispatch,
getState: doGetState
})
Expand Down Expand Up @@ -89,6 +90,7 @@ describe('thunk middleware', () => {
const extraArg = { lol: true }
// @ts-ignore
withExtraArgument(extraArg)({
// @ts-ignore
dispatch: doDispatch,
getState: doGetState
})()((dispatch: any, getState: any, arg: any) => {
Expand Down
37 changes: 33 additions & 4 deletions typescript_test/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ export type State = {

export type Actions = { type: 'FOO' } | { type: 'BAR'; result: number }

export type ThunkResult<R> = ThunkAction<R, State, undefined, Actions>
export type ThunkResult<R> = ThunkAction<
ThunkDispatch<State, undefined, Actions>,
State,
undefined,
R
>

export const initialState: State = {
foo: 'foo'
}

export function fakeReducer(state: State = initialState): State {
export function fakeReducer(
state: State = initialState,
action: Actions
): State {
return state
}

Expand All @@ -36,6 +44,7 @@ store.dispatch(dispatch => {
// @ts-expect-error
dispatch({ type: 'BAR' }, 42)
dispatch({ type: 'BAR', result: 5 })
// @ts-expect-error
store.dispatch({ type: 'BAZ' })
})

Expand All @@ -62,8 +71,10 @@ export function anotherThunkAction(): ThunkResult<string> {
}

store.dispatch({ type: 'FOO' })
// @ts-expect-error
store.dispatch({ type: 'BAR' })
store.dispatch({ type: 'BAR', result: 5 })
// @ts-expect-error
store.dispatch({ type: 'BAZ' })
store.dispatch(testGetState())

Expand All @@ -78,8 +89,10 @@ storeThunkArg.dispatch({ type: 'FOO' })
storeThunkArg.dispatch((dispatch, getState, extraArg) => {
const bar: string = extraArg
store.dispatch({ type: 'FOO' })
// @ts-expect-error
store.dispatch({ type: 'BAR' })
store.dispatch({ type: 'BAR', result: 5 })
// @ts-expect-error
store.dispatch({ type: 'BAZ' })
console.log(extraArg)
})
Expand Down Expand Up @@ -149,12 +162,28 @@ untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve())

// #248: Need a union overload to handle generic dispatched types
function testIssue248() {
const dispatch: ThunkDispatch<any, unknown, AnyAction> = undefined as any
const dispatch: ThunkDispatch<any, unknown, Actions> = store.dispatch

function dispatchWrap(
action: Action | ThunkAction<any, any, unknown, AnyAction>
action: Actions | ThunkAction<any, any, unknown, Actions>
) {
// Should not have an error here thanks to the extra union overload
dispatch(action)

// this errors, because the union overload is not present
// @ts-expect-error
store.dispatch(action)

// workarounds:

// old reliable
store.dispatch(action as any)

// non-ideal, but works
typeof action === 'function'
? store.dispatch(action)
: store.dispatch(action)

// or just assign to ThunkDispatch as above
}
}

0 comments on commit 69d0c0a

Please sign in to comment.