Skip to content

Commit 77775c1

Browse files
phryneasmarkerikson
authored andcommitted
RFC: add "match" method to actionCreator (#239)
1 parent ec87cf0 commit 77775c1

File tree

4 files changed

+172
-29
lines changed

4 files changed

+172
-29
lines changed

src/createAction.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ describe('createAction', () => {
103103
expect(actionCreator('1', '2', '3').payload).toBe('123')
104104
})
105105
})
106+
107+
describe('actionCreator.match', () => {
108+
test('should return true for actions generated by own actionCreator', () => {
109+
const actionCreator = createAction('test')
110+
expect(actionCreator.match(actionCreator())).toBe(true)
111+
})
112+
113+
test('should return true for matching actions', () => {
114+
const actionCreator = createAction('test')
115+
expect(actionCreator.match({ type: 'test' })).toBe(true)
116+
})
117+
118+
test('should return false for other actions', () => {
119+
const actionCreator = createAction('test')
120+
expect(actionCreator.match({ type: 'test-abc' })).toBe(false)
121+
})
122+
})
106123
})
107124

108125
describe('getType', () => {

src/createAction.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,42 +26,47 @@ export type PrepareAction<P> =
2626
export type ActionCreatorWithPreparedPayload<
2727
PA extends PrepareAction<any> | void,
2828
T extends string = string
29-
> = WithTypeProperty<
30-
T,
31-
PA extends PrepareAction<infer P>
32-
? (
29+
> = PA extends PrepareAction<infer P>
30+
? WithTypePropertyAndMatch<
31+
(
3332
...args: Parameters<PA>
34-
) => PayloadAction<P, T, MetaOrNever<PA>, ErrorOrNever<PA>>
35-
: void
36-
>
33+
) => PayloadAction<P, T, MetaOrNever<PA>, ErrorOrNever<PA>>,
34+
T,
35+
P,
36+
MetaOrNever<PA>,
37+
ErrorOrNever<PA>
38+
>
39+
: void
3740

3841
export type ActionCreatorWithOptionalPayload<
3942
P,
4043
T extends string = string
41-
> = WithTypeProperty<
42-
T,
44+
> = WithTypePropertyAndMatch<
4345
{
4446
(payload?: undefined): PayloadAction<undefined, T>
4547
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
46-
}
48+
},
49+
T,
50+
P | undefined
4751
>
4852

4953
export type ActionCreatorWithoutPayload<
5054
T extends string = string
51-
> = WithTypeProperty<T, () => PayloadAction<undefined, T>>
55+
> = WithTypePropertyAndMatch<() => PayloadAction<undefined, T>, T, undefined>
5256

5357
export type ActionCreatorWithPayload<
5458
P,
5559
T extends string = string
56-
> = WithTypeProperty<
57-
T,
60+
> = WithTypePropertyAndMatch<
5861
IsUnknownOrNonInferrable<
5962
P,
6063
// TS < 3.5 infers non-inferrable types to {}, which does not take `null`. This enforces `undefined` instead.
6164
<PT extends unknown>(payload: PT) => PayloadAction<PT, T>,
6265
// default behaviour
6366
<PT extends P>(payload: PT) => PayloadAction<PT, T>
64-
>
67+
>,
68+
T,
69+
P
6570
>
6671

6772
/**
@@ -134,6 +139,9 @@ export function createAction(type: string, prepareAction?: Function) {
134139

135140
actionCreator.type = type
136141

142+
actionCreator.match = (action: Action<unknown>): action is PayloadAction =>
143+
action.type === type
144+
137145
return actionCreator
138146
}
139147

@@ -161,10 +169,22 @@ type WithOptional<M, E, T> = T &
161169
([M] extends [never] ? {} : { meta: M }) &
162170
([E] extends [never] ? {} : { error: E })
163171

164-
type WithTypeProperty<T, MergeIn> = {
172+
type WithTypeProperty<MergeIn, T extends string> = {
165173
type: T
166174
} & MergeIn
167175

176+
type WithMatch<MergeIn, T extends string, P, M = never, E = never> = {
177+
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
178+
} & MergeIn
179+
180+
type WithTypePropertyAndMatch<
181+
MergeIn,
182+
T extends string,
183+
P,
184+
M = never,
185+
E = never
186+
> = WithTypeProperty<WithMatch<MergeIn, T, P, M, E>, T>
187+
168188
type IfPrepareActionMethodProvided<
169189
PA extends PrepareAction<any> | void,
170190
True,

type-tests/files/createAction.typetest.ts

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,29 +213,109 @@ function expectType<T>(p: T): T {
213213
*/
214214
{
215215
const action = createAction<{ input?: string }>('ACTION')
216-
const t: string|undefined = action({input: ""}).payload.input;
217-
216+
const t: string | undefined = action({ input: '' }).payload.input
217+
218218
// typings:expect-error
219-
const u: number = action({input: ""}).payload.input;
219+
const u: number = action({ input: '' }).payload.input
220220
// typings:expect-error
221-
const v: number = action({input: 3}).payload.input;
221+
const v: number = action({ input: 3 }).payload.input
222222
}
223+
223224
/*
224225
* regression test for https://github.com/reduxjs/redux-starter-kit/issues/224
225226
*/
226227
{
227-
const oops = createAction('oops', (x: any) => ({ payload: x, error: x, meta: x }))
228+
const oops = createAction('oops', (x: any) => ({
229+
payload: x,
230+
error: x,
231+
meta: x
232+
}))
228233

229-
type Ret = ReturnType<typeof oops>;
234+
type Ret = ReturnType<typeof oops>
230235

231-
const payload: IsAny<Ret['payload'], true, false> = true;
232-
const error: IsAny<Ret['error'], true, false> = true;
233-
const meta: IsAny<Ret['meta'], true, false> = true;
236+
const payload: IsAny<Ret['payload'], true, false> = true
237+
const error: IsAny<Ret['error'], true, false> = true
238+
const meta: IsAny<Ret['meta'], true, false> = true
234239

235240
// typings:expect-error
236-
const payloadNotAny: IsAny<Ret['payload'], true, false> = false;
241+
const payloadNotAny: IsAny<Ret['payload'], true, false> = false
237242
// typings:expect-error
238-
const errorNotAny: IsAny<Ret['error'], true, false> = false;
243+
const errorNotAny: IsAny<Ret['error'], true, false> = false
239244
// typings:expect-error
240-
const metaNotAny: IsAny<Ret['meta'], true, false> = false;
241-
}
245+
const metaNotAny: IsAny<Ret['meta'], true, false> = false
246+
}
247+
248+
/**
249+
* Test: createAction.match()
250+
*/
251+
{
252+
// simple use case
253+
{
254+
const actionCreator = createAction<string, 'test'>('test')
255+
const x: Action<unknown> = {} as any
256+
if (actionCreator.match(x)) {
257+
expectType<'test'>(x.type)
258+
expectType<string>(x.payload)
259+
} else {
260+
// typings:expect-error
261+
expectType<'test'>(x.type)
262+
// typings:expect-error
263+
expectType<any>(x.payload)
264+
}
265+
}
266+
267+
// special case: optional argument
268+
{
269+
const actionCreator = createAction<string | undefined, 'test'>('test')
270+
const x: Action<unknown> = {} as any
271+
if (actionCreator.match(x)) {
272+
expectType<'test'>(x.type)
273+
expectType<string | undefined>(x.payload)
274+
}
275+
}
276+
277+
// special case: without argument
278+
{
279+
const actionCreator = createAction('test')
280+
const x: Action<unknown> = {} as any
281+
if (actionCreator.match(x)) {
282+
expectType<'test'>(x.type)
283+
// typings:expect-error
284+
expectType<{}>(x.payload)
285+
}
286+
}
287+
288+
// special case: with prepareAction
289+
{
290+
const actionCreator = createAction('test', () => ({
291+
payload: '',
292+
meta: '',
293+
error: false
294+
}))
295+
const x: Action<unknown> = {} as any
296+
if (actionCreator.match(x)) {
297+
expectType<'test'>(x.type)
298+
expectType<string>(x.payload)
299+
expectType<string>(x.meta)
300+
expectType<boolean>(x.error)
301+
// typings:expect-error
302+
expectType<number>(x.payload)
303+
// typings:expect-error
304+
expectType<number>(x.meta)
305+
// typings:expect-error
306+
expectType<number>(x.error)
307+
}
308+
}
309+
// potential use: as array filter
310+
{
311+
const actionCreator = createAction<string, 'test'>('test')
312+
const x: Array<Action<unknown>> = []
313+
expectType<Array<PayloadAction<string, 'test'>>>(
314+
x.filter(actionCreator.match)
315+
)
316+
// typings:expect-error
317+
expectType<Array<PayloadAction<number, 'test'>>>(
318+
x.filter(actionCreator.match)
319+
)
320+
}
321+
}

type-tests/files/createSlice.typetest.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AnyAction, Reducer } from 'redux'
1+
import { AnyAction, Reducer, Action } from 'redux'
22
import { createSlice, PayloadAction, createAction } from '../../src'
33

44
function expectType<T>(t: T) {
@@ -246,3 +246,29 @@ function expectType<T>(t: T) {
246246
mySlice.actions.setName('asd')
247247
mySlice.actions.setName(5)
248248
}
249+
250+
/**
251+
* Test: actions.x.match()
252+
*/
253+
{
254+
const mySlice = createSlice({
255+
name: 'name',
256+
initialState: { name: 'test' },
257+
reducers: {
258+
setName: (state, action: PayloadAction<string>) => {
259+
state.name = action.payload
260+
}
261+
}
262+
})
263+
264+
const x: Action<unknown> = {} as any
265+
if (mySlice.actions.setName.match(x)) {
266+
expectType<string>(x.type)
267+
expectType<string>(x.payload)
268+
} else {
269+
// typings:expect-error
270+
expectType<string>(x.type)
271+
// typings:expect-error
272+
expectType<string>(x.payload)
273+
}
274+
}

0 commit comments

Comments
 (0)