Skip to content

Commit d17b301

Browse files
committed
Failing attempt to improve listener typings
1 parent f5ef360 commit d17b301

File tree

2 files changed

+66
-26
lines changed

2 files changed

+66
-26
lines changed

packages/action-listener-middleware/src/index.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ interface TypedActionCreator<Type extends string> {
2121
match: MatchFunction<any>
2222
}
2323

24-
type ListenerPredicate<Action extends AnyAction, State = unknown> = (
24+
type ListenerPredicate<Action extends AnyAction, State> = (
2525
action: Action,
26-
state?: State
26+
currentState?: State,
27+
originalState?: State
2728
) => boolean
2829

2930
type MatchFunction<T> = (v: any) => v is T
@@ -38,6 +39,16 @@ export const hasMatchFunction = <T>(
3839
return v && typeof (v as HasMatchFunction<T>).match === 'function'
3940
}
4041

42+
export const isActionCreator = (
43+
item: Function
44+
): item is TypedActionCreator<any> => {
45+
return (
46+
typeof item === 'function' &&
47+
typeof (item as any).type === 'string' &&
48+
hasMatchFunction(item as any)
49+
)
50+
}
51+
4152
/** @public */
4253
export type Matcher<T> = HasMatchFunction<T> | MatchFunction<T>
4354

@@ -208,7 +219,7 @@ const actualMiddlewarePhases = ['beforeReducer', 'afterReducer'] as const
208219
* @alpha
209220
*/
210221
export function createActionListenerMiddleware<
211-
S,
222+
S = any,
212223
// TODO Carry through the thunk extra arg somehow?
213224
D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>,
214225
ExtraArgument = unknown
@@ -218,7 +229,7 @@ export function createActionListenerMiddleware<
218229
listener: ActionListener<any, S, D, any>
219230
unsubscribe: () => void
220231
type?: string
221-
predicate: ListenerPredicate<any>
232+
predicate: ListenerPredicate<any, any>
222233
}
223234

224235
const listenerMap = new Map<string, ListenerEntry>()
@@ -256,11 +267,12 @@ export function createActionListenerMiddleware<
256267
const getOriginalState = () => originalState
257268

258269
for (const currentPhase of actualMiddlewarePhases) {
259-
let stateNow = api.getState()
270+
let currentState = api.getState()
260271
for (let entry of listenerMap.values()) {
261272
const runThisPhase =
262273
entry.when === 'both' || entry.when === currentPhase
263-
const runListener = runThisPhase && entry.predicate(action, stateNow)
274+
const runListener =
275+
runThisPhase && entry.predicate(action, currentState, originalState)
264276
if (!runListener) {
265277
continue
266278
}
@@ -306,30 +318,33 @@ export function createActionListenerMiddleware<
306318
// eslint-disable-next-line no-redeclare
307319
function addListener<
308320
MA extends AnyAction,
309-
M extends ListenerPredicate<MA>,
321+
M extends MatchFunction<MA>,
310322
O extends ActionListenerOptions
311323
>(
312324
matcher: M,
313-
listener: ActionListener<AnyAction, S, D, O>,
325+
listener: ActionListener<GuardedType<M>, S, D, O>,
314326
options?: O
315327
): Unsubscribe
316328
// eslint-disable-next-line no-redeclare
317329
function addListener<
318330
MA extends AnyAction,
319-
M extends MatchFunction<MA>,
331+
M extends ListenerPredicate<MA, S>,
320332
O extends ActionListenerOptions
321333
>(
322334
matcher: M,
323-
listener: ActionListener<GuardedType<M>, S, D, O>,
335+
listener: ActionListener<AnyAction, S, D, O>,
324336
options?: O
325337
): Unsubscribe
326338
// eslint-disable-next-line no-redeclare
327339
function addListener(
328-
typeOrActionCreator: string | TypedActionCreator<any>,
340+
typeOrActionCreator:
341+
| string
342+
| TypedActionCreator<any>
343+
| ListenerPredicate<any, any>,
329344
listener: ActionListener<AnyAction, S, D, any>,
330345
options?: ActionListenerOptions
331346
): Unsubscribe {
332-
let predicate: ListenerPredicate<any>
347+
let predicate: ListenerPredicate<any, any>
333348
let type: string | undefined
334349

335350
let entry = findListenerEntry(
@@ -340,11 +355,16 @@ export function createActionListenerMiddleware<
340355
if (typeof typeOrActionCreator === 'string') {
341356
type = typeOrActionCreator
342357
predicate = (action: any) => action.type === type
343-
} else if (typeof typeOrActionCreator.type === 'string') {
344-
type = typeOrActionCreator.type
345-
predicate = typeOrActionCreator.match
346358
} else {
347-
predicate = typeOrActionCreator as unknown as ListenerPredicate<any>
359+
if (isActionCreator(typeOrActionCreator)) {
360+
type = typeOrActionCreator.type
361+
predicate = typeOrActionCreator.match
362+
} else {
363+
predicate = typeOrActionCreator as unknown as ListenerPredicate<
364+
any,
365+
any
366+
>
367+
}
348368
}
349369

350370
const id = nanoid()

packages/action-listener-middleware/src/tests/listenerMiddleware.test.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,20 @@ const noop = () => {}
2626

2727
describe('createActionListenerMiddleware', () => {
2828
let store = configureStore({
29-
reducer: () => ({}),
29+
reducer: () => 42,
3030
middleware: (gDM) => gDM().prepend(createActionListenerMiddleware()),
3131
})
3232

33+
interface CounterState {
34+
value: number
35+
}
36+
3337
const counterSlice = createSlice({
3438
name: 'counter',
35-
initialState: 0,
39+
initialState: { value: 0 } as CounterState,
3640
reducers: {
3741
increment(state) {
38-
return state + 1
42+
state.value += 1
3943
},
4044
},
4145
})
@@ -175,22 +179,38 @@ describe('createActionListenerMiddleware', () => {
175179
middleware: (gDM) => gDM().prepend(middleware),
176180
})
177181

178-
let listenerCalls = 0
182+
let listener1Calls = 0
179183

180184
middleware.addListener(
181-
(action, state) => {
182-
return state > 1
185+
// TODO Can't figure out how to get `any` or a real state type instead of `unknown` here
186+
// @ts-expect-error
187+
(action: AnyAction, state: CounterState) => {
188+
return state.value > 1
189+
},
190+
(action, listenerApi) => {
191+
listener1Calls++
192+
}
193+
)
194+
195+
let listener2Calls = 0
196+
197+
middleware.addListener(
198+
// @ts-expect-error
199+
(action, state: CounterState, prevState: CounterState) => {
200+
return state.value > 1 && prevState.value % 2 === 0
183201
},
184202
(action, listenerApi) => {
185-
listenerCalls++
203+
listener2Calls++
186204
}
187205
)
188206

189207
store.dispatch(increment())
190208
store.dispatch(increment())
191209
store.dispatch(increment())
210+
store.dispatch(increment())
192211

193-
expect(listenerCalls).toBe(2)
212+
expect(listener1Calls).toBe(3)
213+
expect(listener2Calls).toBe(1)
194214
})
195215

196216
test('subscribing with the same listener will not make it trigger twice (like EventTarget.addEventListener())', () => {
@@ -284,11 +304,11 @@ describe('createActionListenerMiddleware', () => {
284304
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
285305
})
286306

287-
const unforwaredActions: [string, AnyAction][] = [
307+
const unforwardedActions: [string, AnyAction][] = [
288308
['addListenerAction', addListenerAction(testAction1, noop)],
289309
['removeListenerAction', removeListenerAction(testAction1, noop)],
290310
]
291-
test.each(unforwaredActions)(
311+
test.each(unforwardedActions)(
292312
'"%s" is not forwarded to the reducer',
293313
(_, action) => {
294314
reducer.mockClear()

0 commit comments

Comments
 (0)