From 69d0c0ab83d3eac1f4833989466b677980bb6892 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 29 Jun 2024 20:57:07 +0100 Subject: [PATCH 1/4] Rework types to better support getting *full* dispatch type --- src/index.ts | 1 - src/types.ts | 66 ++++++++++++++++++++--------------- test/test.ts | 2 ++ typescript_test/typescript.ts | 37 +++++++++++++++++--- 4 files changed, 73 insertions(+), 33 deletions(-) diff --git a/src/index.ts b/src/index.ts index 58bab3d..ff6b45d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import type { Action, AnyAction } from 'redux' - import type { ThunkMiddleware } from './types' export type { diff --git a/src/types.ts b/src/types.ts index 973e4ac..9cc9c62 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 ThunkDispatch, 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 > 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..84296e4 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,28 @@ untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve()) // #248: Need a union overload to handle generic dispatched types function testIssue248() { - const dispatch: ThunkDispatch = undefined as any + const dispatch: ThunkDispatch = store.dispatch function dispatchWrap( - action: Action | ThunkAction + action: Actions | ThunkAction ) { // 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 } } From 0b0aed84783bef5340b5b933671f26a263a8ec59 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 29 Jun 2024 21:01:00 +0100 Subject: [PATCH 2/4] move reassignment to a type of workaround --- typescript_test/typescript.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/typescript_test/typescript.ts b/typescript_test/typescript.ts index 84296e4..18c26d7 100644 --- a/typescript_test/typescript.ts +++ b/typescript_test/typescript.ts @@ -162,20 +162,20 @@ untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve()) // #248: Need a union overload to handle generic dispatched types function testIssue248() { - const dispatch: ThunkDispatch = store.dispatch - function dispatchWrap( action: Actions | ThunkAction ) { - // 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: + // 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) @@ -183,7 +183,5 @@ function testIssue248() { typeof action === 'function' ? store.dispatch(action) : store.dispatch(action) - - // or just assign to ThunkDispatch as above } } From 61e9e4d9a8296d98b235a1f59e9a7dd24b40f03c Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sat, 29 Jun 2024 21:08:05 +0100 Subject: [PATCH 3/4] loosen requirements for Dispatch parameter --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 9cc9c62..c693230 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,7 +60,7 @@ export type ThunkDispatch< * (if specified when setting up the Thunk middleware) */ export type ThunkAction< - Dispatch extends ThunkDispatch, + Dispatch extends ThunkOverload, State, ExtraThunkArg, ReturnType From fdb2e15423c18abc6b422e7ccbbd16f64633f800 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 30 Jun 2024 00:27:29 +0100 Subject: [PATCH 4/4] tweak type requirements and export ThunkOverload --- src/index.ts | 3 ++- src/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index ff6b45d..84ac8f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ 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 c693230..02dd5a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -97,5 +97,5 @@ export type ThunkMiddleware< > = Middleware< ThunkOverload, State, - ThunkDispatch + Dispatch >