Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(action): support optional error field #222

Merged
merged 3 commits into from
Oct 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/createAction.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ console.log(addTodo('Write more docs'))
**/
```

If provided, all arguments from the action creator will be passed to the prepare callback, and it should return an object with the `payload` field (otherwise the payload of created actions will be `undefined`). Additionally, the object can have a `meta` field that will also be added to created actions. This may contain extra information about the action. These two fields (`payload` and `meta`) adhere to the specification of [Flux Standard Actions](https://github.com/redux-utilities/flux-standard-action#actions).
If provided, all arguments from the action creator will be passed to the prepare callback, and it should return an object with the `payload` field (otherwise the payload of created actions will be `undefined`). Additionally, the object can have a `meta` and/or an `error` field that will also be added to created actions. `meta` may contain extra information about the action, `error` may contain details about the action failure. These three fields (`payload`, `meta` and `error`) adhere to the specification of [Flux Standard Actions](https://github.com/redux-utilities/flux-standard-action#actions).

**Note:** The type field will be added automatically.

Expand Down
44 changes: 44 additions & 0 deletions src/createAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,50 @@ describe('createAction', () => {
})
})

describe('when passing a prepareAction method returning a payload and error', () => {
it('should use the payload returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
payload: a * 2,
error: true
}))
expect(actionCreator(5).payload).toBe(10)
})
it('should use the error returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
payload: a * 2,
error: true
}))
expect(actionCreator(10).error).toBe(true)
})
})

describe('when passing a prepareAction method returning a payload, meta and error', () => {
it('should use the payload returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
payload: a * 2,
meta: a / 2,
error: true
}))
expect(actionCreator(5).payload).toBe(10)
})
it('should use the error returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
payload: a * 2,
meta: a / 2,
error: true
}))
expect(actionCreator(10).error).toBe(true)
})
it('should use the meta returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
payload: a * 2,
meta: a / 2,
error: true
}))
expect(actionCreator(10).meta).toBe(5)
})
})

describe('when passing a prepareAction that accepts multiple arguments', () => {
it('should pass all arguments of the resulting actionCreator to prepareAction', () => {
const actionCreator = createAction(
Expand Down
31 changes: 24 additions & 7 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,29 @@ import { IsUnknownOrNonInferrable } from './tsHelpers'
* @template P The type of the action's payload.
* @template T the type used for the action type.
* @template M The type of the action's meta (optional)
* @template E The type of the action's error (optional)
*/
export type PayloadAction<
P = void,
T extends string = string,
M = void
> = WithOptionalMeta<M, WithPayload<P, Action<T>>>
M = void,
E = void
> = WithOptional<M, E, WithPayload<P, Action<T>>>

export type PrepareAction<P> =
| ((...args: any[]) => { payload: P })
| ((...args: any[]) => { payload: P; meta: any })
| ((...args: any[]) => { payload: P; meta: any; error: any })

export type ActionCreatorWithPreparedPayload<
PA extends PrepareAction<any> | void,
T extends string = string
> = WithTypeProperty<
T,
PA extends PrepareAction<infer P>
? (...args: Parameters<PA>) => PayloadAction<P, T, MetaOrVoid<PA>>
? (
...args: Parameters<PA>
) => PayloadAction<P, T, MetaOrVoid<PA>, ErrorOrVoid<PA>>
: void
>

Expand Down Expand Up @@ -113,9 +118,13 @@ export function createAction(type: string, prepareAction?: Function) {
if (!prepared) {
throw new Error('prepareAction did not return an object')
}
return 'meta' in prepared
? { type, payload: prepared.payload, meta: prepared.meta }
: { type, payload: prepared.payload }

return {
type,
payload: prepared.payload,
...('meta' in prepared && { meta: prepared.meta }),
...('error' in prepared && { error: prepared.error })
}
}
return { type, payload: args[0] }
}
Expand Down Expand Up @@ -147,7 +156,9 @@ type Diff<T, U> = T extends U ? never : T

type WithPayload<P, T> = T & { payload: P }

type WithOptionalMeta<M, T> = T & ([M] extends [void] ? {} : { meta: M })
type WithOptional<M, E, T> = T &
([M] extends [void] ? {} : { meta: M }) &
([E] extends [void] ? {} : { error: E })

type WithTypeProperty<T, MergeIn> = {
type: T
Expand All @@ -165,6 +176,12 @@ type MetaOrVoid<PA extends PrepareAction<any>> = ReturnType<PA> extends {
? M
: void

type ErrorOrVoid<PA extends PrepareAction<any>> = ReturnType<PA> extends {
error: infer E
}
? E
: void

type IfMaybeUndefined<P, True, False> = [undefined] extends [P] ? True : False

type IfVoid<P, True, False> = [void] extends [P] ? True : False
34 changes: 34 additions & 0 deletions type-tests/files/createAction.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ function expectType<T>(p: T): T {

// typings:expect-error
expectType<string>(strLenAction('test').payload)
// typings:expect-error
const error: any = strLenAction('test').error
}

/*
Expand All @@ -171,6 +173,38 @@ function expectType<T>(p: T): T {

// typings:expect-error
expectType<string>(strLenMetaAction('test').meta)
// typings:expect-error
const error: any = strLenMetaAction('test').error
}

/*
* Test: adding boolean error with prepareAction
*/
{
const boolErrorAction = createAction('boolError', (payload: string) => ({
payload,
error: true
}))

expectType<boolean>(boolErrorAction('test').error)

// typings:expect-error
expectType<string>(boolErrorAction('test').error)
tvanier marked this conversation as resolved.
Show resolved Hide resolved
}

/*
* Test: adding string error with prepareAction
*/
{
const strErrorAction = createAction('strError', (payload: string) => ({
payload,
error: 'this is an error'
}))

expectType<string>(strErrorAction('test').error)

// typings:expect-error
expectType<boolean>(strErrorAction('test').error)
}

/*
Expand Down