diff --git a/src/index.ts b/src/index.ts index 58bab3d..84ac8f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/types.ts b/src/types.ts index 973e4ac..02dd5a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 { + ( + thunkAction: ThunkAction + ): ReturnType +} /** * The dispatch method as modified by React-Thunk; overloaded so that you can @@ -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 */ - ( - thunkAction: ThunkAction - ): ReturnType - - // 2) The base overload. - /** Accepts a standard action object, and returns that action object */ - (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 */ - ( - action: Action | ThunkAction - ): Action | ReturnType -} +> = ThunkOverload & + Dispatch & + // 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 + (( + action: + | Action + | ThunkAction< + ThunkDispatch, + State, + ExtraThunkArg, + BasicAction + > + ) => Action | ReturnType) /** * A "thunk" action (a callback function that can be dispatched to the Redux @@ -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, - BasicAction extends Action + ReturnType > = ( - dispatch: ThunkDispatch, + dispatch: Dispatch, getState: () => State, extraArgument: ExtraThunkArg ) => ReturnType @@ -82,10 +92,10 @@ export type ThunkActionDispatch< */ export type ThunkMiddleware< State = any, - BasicAction extends Action = AnyAction, + BasicAction extends Action = UnknownAction, ExtraThunkArg = undefined > = Middleware< - ThunkDispatch, + ThunkOverload, State, - ThunkDispatch + Dispatch > diff --git a/test/test.ts b/test/test.ts index 15b403d..998bc01 100644 --- a/test/test.ts +++ b/test/test.ts @@ -4,6 +4,7 @@ describe('thunk middleware', () => { const doDispatch = () => {} const doGetState = () => 42 const nextHandler = thunkMiddleware({ + // @ts-ignore dispatch: doDispatch, getState: doGetState }) @@ -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) => { diff --git a/typescript_test/typescript.ts b/typescript_test/typescript.ts index c6a52b6..18c26d7 100644 --- a/typescript_test/typescript.ts +++ b/typescript_test/typescript.ts @@ -16,13 +16,21 @@ export type State = { export type Actions = { type: 'FOO' } | { type: 'BAR'; result: number } -export type ThunkResult = ThunkAction +export type ThunkResult = ThunkAction< + ThunkDispatch, + 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 } @@ -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' }) }) @@ -62,8 +71,10 @@ export function anotherThunkAction(): ThunkResult { } 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()) @@ -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) }) @@ -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 = undefined as any - function dispatchWrap( - action: Action | ThunkAction + action: Actions | ThunkAction ) { + // 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 = store.dispatch dispatch(action) + + // old reliable + store.dispatch(action as any) + + // non-ideal, but works + typeof action === 'function' + ? store.dispatch(action) + : store.dispatch(action) } }