Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework types to better support getting *full* dispatch type #372

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Action, AnyAction } from 'redux'

import type { ThunkMiddleware } from './types'

export type {
ThunkAction,
ThunkDispatch,
ThunkActionDispatch,
ThunkMiddleware
ThunkMiddleware,
ThunkOverload
} from './types'

/** A function that accepts a potential "extra argument" value to be injected later,
Expand Down
68 changes: 39 additions & 29 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>
): 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 ThunkOverload<State, ExtraThunkArg>,
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>
Dispatch<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: 32 additions & 5 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,26 @@ 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

function dispatchWrap(
action: Action | ThunkAction<any, any, unknown, AnyAction>
action: Actions | ThunkAction<any, any, unknown, Actions>
) {
// this errors, because the union overload is not present
// @ts-expect-error
store.dispatch(action)

// workarounds:

// assign to ThunkDispatch type
// Should not have an error here thanks to the extra union overload
const dispatch: ThunkDispatch<any, unknown, Actions> = store.dispatch
dispatch(action)

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

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