Skip to content

Commit

Permalink
RFC: add "match" method to actionCreator (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas authored and markerikson committed Nov 2, 2019
1 parent ec87cf0 commit 77775c1
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 29 deletions.
17 changes: 17 additions & 0 deletions src/createAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ describe('createAction', () => {
expect(actionCreator('1', '2', '3').payload).toBe('123')
})
})

describe('actionCreator.match', () => {
test('should return true for actions generated by own actionCreator', () => {
const actionCreator = createAction('test')
expect(actionCreator.match(actionCreator())).toBe(true)
})

test('should return true for matching actions', () => {
const actionCreator = createAction('test')
expect(actionCreator.match({ type: 'test' })).toBe(true)
})

test('should return false for other actions', () => {
const actionCreator = createAction('test')
expect(actionCreator.match({ type: 'test-abc' })).toBe(false)
})
})
})

describe('getType', () => {
Expand Down
50 changes: 35 additions & 15 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,47 @@ export type PrepareAction<P> =
export type ActionCreatorWithPreparedPayload<
PA extends PrepareAction<any> | void,
T extends string = string
> = WithTypeProperty<
T,
PA extends PrepareAction<infer P>
? (
> = PA extends PrepareAction<infer P>
? WithTypePropertyAndMatch<
(
...args: Parameters<PA>
) => PayloadAction<P, T, MetaOrNever<PA>, ErrorOrNever<PA>>
: void
>
) => PayloadAction<P, T, MetaOrNever<PA>, ErrorOrNever<PA>>,
T,
P,
MetaOrNever<PA>,
ErrorOrNever<PA>
>
: void

export type ActionCreatorWithOptionalPayload<
P,
T extends string = string
> = WithTypeProperty<
T,
> = WithTypePropertyAndMatch<
{
(payload?: undefined): PayloadAction<undefined, T>
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
}
},
T,
P | undefined
>

export type ActionCreatorWithoutPayload<
T extends string = string
> = WithTypeProperty<T, () => PayloadAction<undefined, T>>
> = WithTypePropertyAndMatch<() => PayloadAction<undefined, T>, T, undefined>

export type ActionCreatorWithPayload<
P,
T extends string = string
> = WithTypeProperty<
T,
> = WithTypePropertyAndMatch<
IsUnknownOrNonInferrable<
P,
// TS < 3.5 infers non-inferrable types to {}, which does not take `null`. This enforces `undefined` instead.
<PT extends unknown>(payload: PT) => PayloadAction<PT, T>,
// default behaviour
<PT extends P>(payload: PT) => PayloadAction<PT, T>
>
>,
T,
P
>

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

actionCreator.type = type

actionCreator.match = (action: Action<unknown>): action is PayloadAction =>
action.type === type

return actionCreator
}

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

type WithTypeProperty<T, MergeIn> = {
type WithTypeProperty<MergeIn, T extends string> = {
type: T
} & MergeIn

type WithMatch<MergeIn, T extends string, P, M = never, E = never> = {
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
} & MergeIn

type WithTypePropertyAndMatch<
MergeIn,
T extends string,
P,
M = never,
E = never
> = WithTypeProperty<WithMatch<MergeIn, T, P, M, E>, T>

type IfPrepareActionMethodProvided<
PA extends PrepareAction<any> | void,
True,
Expand Down
106 changes: 93 additions & 13 deletions type-tests/files/createAction.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,29 +213,109 @@ function expectType<T>(p: T): T {
*/
{
const action = createAction<{ input?: string }>('ACTION')
const t: string|undefined = action({input: ""}).payload.input;
const t: string | undefined = action({ input: '' }).payload.input

// typings:expect-error
const u: number = action({input: ""}).payload.input;
const u: number = action({ input: '' }).payload.input
// typings:expect-error
const v: number = action({input: 3}).payload.input;
const v: number = action({ input: 3 }).payload.input
}

/*
* regression test for https://github.com/reduxjs/redux-starter-kit/issues/224
*/
{
const oops = createAction('oops', (x: any) => ({ payload: x, error: x, meta: x }))
const oops = createAction('oops', (x: any) => ({
payload: x,
error: x,
meta: x
}))

type Ret = ReturnType<typeof oops>;
type Ret = ReturnType<typeof oops>

const payload: IsAny<Ret['payload'], true, false> = true;
const error: IsAny<Ret['error'], true, false> = true;
const meta: IsAny<Ret['meta'], true, false> = true;
const payload: IsAny<Ret['payload'], true, false> = true
const error: IsAny<Ret['error'], true, false> = true
const meta: IsAny<Ret['meta'], true, false> = true

// typings:expect-error
const payloadNotAny: IsAny<Ret['payload'], true, false> = false;
const payloadNotAny: IsAny<Ret['payload'], true, false> = false
// typings:expect-error
const errorNotAny: IsAny<Ret['error'], true, false> = false;
const errorNotAny: IsAny<Ret['error'], true, false> = false
// typings:expect-error
const metaNotAny: IsAny<Ret['meta'], true, false> = false;
}
const metaNotAny: IsAny<Ret['meta'], true, false> = false
}

/**
* Test: createAction.match()
*/
{
// simple use case
{
const actionCreator = createAction<string, 'test'>('test')
const x: Action<unknown> = {} as any
if (actionCreator.match(x)) {
expectType<'test'>(x.type)
expectType<string>(x.payload)
} else {
// typings:expect-error
expectType<'test'>(x.type)
// typings:expect-error
expectType<any>(x.payload)
}
}

// special case: optional argument
{
const actionCreator = createAction<string | undefined, 'test'>('test')
const x: Action<unknown> = {} as any
if (actionCreator.match(x)) {
expectType<'test'>(x.type)
expectType<string | undefined>(x.payload)
}
}

// special case: without argument
{
const actionCreator = createAction('test')
const x: Action<unknown> = {} as any
if (actionCreator.match(x)) {
expectType<'test'>(x.type)
// typings:expect-error
expectType<{}>(x.payload)
}
}

// special case: with prepareAction
{
const actionCreator = createAction('test', () => ({
payload: '',
meta: '',
error: false
}))
const x: Action<unknown> = {} as any
if (actionCreator.match(x)) {
expectType<'test'>(x.type)
expectType<string>(x.payload)
expectType<string>(x.meta)
expectType<boolean>(x.error)
// typings:expect-error
expectType<number>(x.payload)
// typings:expect-error
expectType<number>(x.meta)
// typings:expect-error
expectType<number>(x.error)
}
}
// potential use: as array filter
{
const actionCreator = createAction<string, 'test'>('test')
const x: Array<Action<unknown>> = []
expectType<Array<PayloadAction<string, 'test'>>>(
x.filter(actionCreator.match)
)
// typings:expect-error
expectType<Array<PayloadAction<number, 'test'>>>(
x.filter(actionCreator.match)
)
}
}
28 changes: 27 additions & 1 deletion type-tests/files/createSlice.typetest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyAction, Reducer } from 'redux'
import { AnyAction, Reducer, Action } from 'redux'
import { createSlice, PayloadAction, createAction } from '../../src'

function expectType<T>(t: T) {
Expand Down Expand Up @@ -246,3 +246,29 @@ function expectType<T>(t: T) {
mySlice.actions.setName('asd')
mySlice.actions.setName(5)
}

/**
* Test: actions.x.match()
*/
{
const mySlice = createSlice({
name: 'name',
initialState: { name: 'test' },
reducers: {
setName: (state, action: PayloadAction<string>) => {
state.name = action.payload
}
}
})

const x: Action<unknown> = {} as any
if (mySlice.actions.setName.match(x)) {
expectType<string>(x.type)
expectType<string>(x.payload)
} else {
// typings:expect-error
expectType<string>(x.type)
// typings:expect-error
expectType<string>(x.payload)
}
}

0 comments on commit 77775c1

Please sign in to comment.