Skip to content

Commit 4920bb7

Browse files
committed
Add stopListening.withTypes
1 parent 856b525 commit 4920bb7

File tree

4 files changed

+137
-52
lines changed

4 files changed

+137
-52
lines changed

packages/toolkit/src/listenerMiddleware/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ const safelyNotifyError = (
281281
/**
282282
* @public
283283
*/
284-
export const addListener = createAction(
285-
`${alm}/add`
286-
) as TypedAddListener<unknown>
284+
export const addListener = Object.assign(createAction(`${alm}/add`), {
285+
withTypes: () => addListener,
286+
}) as unknown as TypedAddListener<unknown>
287287

288288
/**
289289
* @public
@@ -293,9 +293,9 @@ export const clearAllListeners = createAction(`${alm}/removeAll`)
293293
/**
294294
* @public
295295
*/
296-
export const removeListener = createAction(
297-
`${alm}/remove`
298-
) as TypedRemoveListener<unknown>
296+
export const removeListener = Object.assign(createAction(`${alm}/remove`), {
297+
withTypes: () => removeListener,
298+
}) as unknown as TypedRemoveListener<unknown>
299299

300300
const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
301301
console.error(`${alm}/error`, ...args)
@@ -373,6 +373,10 @@ export const createListenerMiddleware = <
373373
return !!entry
374374
}
375375

376+
Object.assign(stopListening, {
377+
withTypes: () => stopListening,
378+
})
379+
376380
const notifyListener = async (
377381
entry: ListenerEntry<unknown, Dispatch<UnknownAction>>,
378382
action: unknown,

packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test-d.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import type { TypedStartListening } from '@reduxjs/toolkit'
1+
import type {
2+
Action,
3+
ThunkAction,
4+
TypedAddListener,
5+
TypedRemoveListener,
6+
TypedStartListening,
7+
TypedStopListening,
8+
} from '@reduxjs/toolkit'
29
import {
10+
addListener,
311
configureStore,
412
createAsyncThunk,
513
createListenerMiddleware,
614
createSlice,
15+
removeListener,
716
} from '@reduxjs/toolkit'
8-
import type { Action } from 'redux'
9-
import type { ThunkAction } from 'redux-thunk'
1017
import { expectTypeOf } from 'vitest'
1118

1219
export interface CounterState {
@@ -44,13 +51,13 @@ export const incrementAsync = createAsyncThunk(
4451

4552
const { increment } = counterSlice.actions
4653

47-
const counterStore = configureStore({
54+
const store = configureStore({
4855
reducer: counterSlice.reducer,
4956
})
5057

51-
type AppStore = typeof counterStore
52-
type AppDispatch = typeof counterStore.dispatch
53-
type RootState = ReturnType<typeof counterStore.getState>
58+
type AppStore = typeof store
59+
type AppDispatch = typeof store.dispatch
60+
type RootState = ReturnType<typeof store.getState>
5461
type AppThunk<ThunkReturnType = void> = ThunkAction<
5562
ThunkReturnType,
5663
RootState,
@@ -64,30 +71,36 @@ describe('listenerMiddleware.withTypes<RootState, AppDispatch>()', () => {
6471
let done = false
6572

6673
type ExpectedTakeResultType =
67-
| readonly [ReturnType<typeof increment>, CounterState, CounterState]
74+
| [ReturnType<typeof increment>, RootState, RootState]
6875
| null
6976

7077
test('startListening.withTypes', () => {
7178
const startAppListening = listenerMiddleware.startListening.withTypes<
72-
CounterState,
79+
RootState,
7380
AppDispatch
7481
>()
7582

7683
expectTypeOf(startAppListening).toEqualTypeOf<
77-
TypedStartListening<CounterState, AppDispatch>
84+
TypedStartListening<RootState, AppDispatch>
7885
>()
7986

8087
startAppListening({
8188
predicate: increment.match,
82-
effect: async (_, listenerApi) => {
89+
effect: async (action, listenerApi) => {
8390
const stateBefore = listenerApi.getState()
8491

85-
expectTypeOf(stateBefore).toEqualTypeOf<CounterState>()
92+
expectTypeOf(increment).returns.toEqualTypeOf(action)
93+
94+
expectTypeOf(listenerApi.dispatch).toEqualTypeOf<AppDispatch>()
95+
96+
expectTypeOf(stateBefore).toEqualTypeOf<RootState>()
8697

8798
let takeResult = await listenerApi.take(increment.match, timeout)
8899
const stateCurrent = listenerApi.getState()
89100

90-
expectTypeOf(stateCurrent).toEqualTypeOf<CounterState>()
101+
expectTypeOf(takeResult).toEqualTypeOf<ExpectedTakeResultType>()
102+
103+
expectTypeOf(stateCurrent).toEqualTypeOf<RootState>()
91104

92105
timeout = 1
93106
takeResult = await listenerApi.take(increment.match, timeout)
@@ -97,5 +110,40 @@ describe('listenerMiddleware.withTypes<RootState, AppDispatch>()', () => {
97110
})
98111
})
99112

100-
test.todo('addListener.withTypes', () => {})
113+
test('addListener.withTypes', () => {
114+
const addAppListener = addListener.withTypes<RootState, AppDispatch>()
115+
116+
expectTypeOf(addAppListener).toEqualTypeOf<
117+
TypedAddListener<RootState, AppDispatch>
118+
>()
119+
120+
store.dispatch(
121+
addAppListener({
122+
matcher: increment.match,
123+
effect: (action, listenerApi) => {
124+
const state = listenerApi.getState()
125+
126+
expectTypeOf(state).toEqualTypeOf<RootState>()
127+
128+
expectTypeOf(listenerApi.dispatch).toEqualTypeOf<AppDispatch>()
129+
},
130+
})
131+
)
132+
})
133+
134+
test('removeListener.withTypes', () => {
135+
const removeAppListener = removeListener.withTypes<RootState, AppDispatch>()
136+
137+
expectTypeOf(removeAppListener).toEqualTypeOf<
138+
TypedRemoveListener<RootState, AppDispatch>
139+
>()
140+
})
141+
142+
test('stopListening.withTypes', () => {
143+
const stopAppListening = listenerMiddleware.stopListening.withTypes<RootState, AppDispatch>()
144+
145+
expectTypeOf(stopAppListening).toEqualTypeOf<
146+
TypedStopListening<RootState, AppDispatch>
147+
>()
148+
})
101149
})

packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ describe('createListenerMiddleware', () => {
564564
typeof store.getState,
565565
typeof store.dispatch
566566
>,
567-
'effect'
567+
'effect' | 'withTypes'
568568
>
569569
][] = [
570570
['predicate', { predicate: () => true }],

packages/toolkit/src/listenerMiddleware/types.ts

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,18 @@ export interface ListenerMiddlewareInstance<
336336
ExtraArgument = unknown
337337
> {
338338
middleware: ListenerMiddleware<StateType, DispatchType, ExtraArgument>
339+
339340
startListening: AddListenerOverloads<
340341
UnsubscribeListener,
341342
StateType,
342343
DispatchType,
343344
ExtraArgument
344-
>
345-
stopListening: RemoveListenerOverloads<StateType, DispatchType>
345+
> &
346+
TypedStartListening<StateType, DispatchType, ExtraArgument>
347+
348+
stopListening: RemoveListenerOverloads<StateType, DispatchType> &
349+
TypedStopListening<StateType, DispatchType>
350+
346351
/**
347352
* Unsubscribes all listeners, cancels running listeners and tasks.
348353
*/
@@ -494,11 +499,6 @@ export interface AddListenerOverloads<
494499
>
495500
} & AdditionalOptions
496501
): Return
497-
498-
withTypes: <
499-
OverrideStateType extends StateType,
500-
OverrideDispatchType extends DispatchType
501-
>() => TypedStartListening<OverrideStateType, OverrideDispatchType>
502502
}
503503

504504
/** @public */
@@ -515,12 +515,7 @@ export type RemoveListenerOverloads<
515515
DispatchType,
516516
any,
517517
UnsubscribeListenerOptions
518-
> & {
519-
withTypes: <
520-
OverrideStateType extends StateType,
521-
OverrideDispatchType extends DispatchType
522-
>() => TypedRemoveListener<OverrideStateType, OverrideDispatchType>
523-
}
518+
>
524519

525520
/** @public */
526521
export interface RemoveListenerAction<
@@ -566,46 +561,84 @@ export type TypedAddListener<
566561
* A "pre-typed" version of `removeListenerAction`, so the listener args are well-typed */
567562
export type TypedRemoveListener<
568563
StateType,
569-
DispatchType extends ReduxDispatch = ThunkDispatch<StateType, unknown, UnknownAction>,
564+
DispatchType extends ReduxDispatch = ThunkDispatch<
565+
StateType,
566+
unknown,
567+
UnknownAction
568+
>,
570569
Payload = ListenerEntry<StateType, DispatchType>,
571570
T extends string = 'listenerMiddleware/remove'
572-
> = BaseActionCreator<Payload, T> &
573-
AddListenerOverloads<
571+
> = BaseActionCreator<Payload, T> & {
572+
withTypes: <
573+
OverrideStateType extends StateType,
574+
OverrideDispatchType extends DispatchType
575+
>() => TypedRemoveListener<OverrideStateType, OverrideDispatchType>
576+
} & AddListenerOverloads<
574577
PayloadAction<Payload, T>,
575578
StateType,
576579
DispatchType,
577580
any,
578581
UnsubscribeListenerOptions
579582
>
580-
// & {
581-
// withTypes: <
582-
// OverrideStateType extends StateType,
583-
// OverrideDispatchType extends DispatchType
584-
// >() => TypedRemoveListener<>
585-
// }
586583

587584
/**
588585
* @public
589586
* A "pre-typed" version of `middleware.startListening`, so the listener args are well-typed */
590587
export type TypedStartListening<
591-
State,
592-
Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>,
588+
StateType,
589+
DispatchType extends ReduxDispatch = ThunkDispatch<
590+
StateType,
591+
unknown,
592+
UnknownAction
593+
>,
593594
ExtraArgument = unknown
594-
> = AddListenerOverloads<UnsubscribeListener, State, Dispatch, ExtraArgument>
595+
> = AddListenerOverloads<
596+
UnsubscribeListener,
597+
StateType,
598+
DispatchType,
599+
ExtraArgument
600+
> & {
601+
withTypes: <
602+
OverrideStateType extends StateType,
603+
OverrideDispatchType extends DispatchType
604+
>() => TypedStartListening<OverrideStateType, OverrideDispatchType>
605+
}
595606

596607
/** @public
597608
* A "pre-typed" version of `middleware.stopListening`, so the listener args are well-typed */
598609
export type TypedStopListening<
599-
State,
600-
Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>
601-
> = RemoveListenerOverloads<State, Dispatch>
610+
StateType,
611+
DispatchType extends ReduxDispatch = ThunkDispatch<
612+
StateType,
613+
unknown,
614+
UnknownAction
615+
>
616+
> = RemoveListenerOverloads<StateType, DispatchType> & {
617+
withTypes: <
618+
OverrideStateType extends StateType,
619+
OverrideDispatchType extends DispatchType
620+
>() => TypedStopListening<OverrideStateType, OverrideDispatchType>
621+
}
602622

603623
/** @public
604624
* A "pre-typed" version of `createListenerEntry`, so the listener args are well-typed */
605625
export type TypedCreateListenerEntry<
606-
State,
607-
Dispatch extends ReduxDispatch = ThunkDispatch<State, unknown, UnknownAction>
608-
> = AddListenerOverloads<ListenerEntry<State, Dispatch>, State, Dispatch>
626+
StateType,
627+
DispatchType extends ReduxDispatch = ThunkDispatch<
628+
StateType,
629+
unknown,
630+
UnknownAction
631+
>
632+
> = AddListenerOverloads<
633+
ListenerEntry<StateType, DispatchType>,
634+
StateType,
635+
DispatchType
636+
> & {
637+
withTypes: <
638+
OverrideStateType extends StateType,
639+
OverrideDispatchType extends DispatchType
640+
>() => TypedStopListening<OverrideStateType, OverrideDispatchType>
641+
}
609642

610643
/**
611644
* Internal Types

0 commit comments

Comments
 (0)