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 2 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
expectType<boolean>(strLenAction('test').error)
Copy link
Member

@phryneas phryneas Oct 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace this with something like

// typings:expect-error
const x: any = strLenMetaAction('test').error

you want to see that the property does not exist at all, while the

// typings:expect-error 
expectType<boolean>(...)

just ensures that it isn't boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the test as suggested.
Out of curiosity, would it be possible to add Jest expectations here?

  // typings:expect-error
  const error: any = strLenAction('test').error
  expect(error).toBeUndefined()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that code is actually ever run, it should only be compiled to look for compiler warnings. Doing something like this would work better in the normal tests.

We just want to make sure that there's no regression in the types & autocompletion.

}

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

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

/*
* 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