Skip to content

Commit c4bb2ae

Browse files
tvaniermarkerikson
authored andcommitted
feat(action): support optional error field (#222)
* feat(action): support optional error field * feat(action): add type tests for optional error field * feat(action): typings expect-error if no error field
1 parent 107eda9 commit c4bb2ae

File tree

4 files changed

+103
-8
lines changed

4 files changed

+103
-8
lines changed

docs/api/createAction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ console.log(addTodo('Write more docs'))
7979
**/
8080
```
8181
82-
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).
82+
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).
8383
8484
**Note:** The type field will be added automatically.
8585

src/createAction.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,50 @@ describe('createAction', () => {
4848
})
4949
})
5050

51+
describe('when passing a prepareAction method returning a payload and error', () => {
52+
it('should use the payload returned from the prepareAction method', () => {
53+
const actionCreator = createAction('A_TYPE', (a: number) => ({
54+
payload: a * 2,
55+
error: true
56+
}))
57+
expect(actionCreator(5).payload).toBe(10)
58+
})
59+
it('should use the error returned from the prepareAction method', () => {
60+
const actionCreator = createAction('A_TYPE', (a: number) => ({
61+
payload: a * 2,
62+
error: true
63+
}))
64+
expect(actionCreator(10).error).toBe(true)
65+
})
66+
})
67+
68+
describe('when passing a prepareAction method returning a payload, meta and error', () => {
69+
it('should use the payload returned from the prepareAction method', () => {
70+
const actionCreator = createAction('A_TYPE', (a: number) => ({
71+
payload: a * 2,
72+
meta: a / 2,
73+
error: true
74+
}))
75+
expect(actionCreator(5).payload).toBe(10)
76+
})
77+
it('should use the error returned from the prepareAction method', () => {
78+
const actionCreator = createAction('A_TYPE', (a: number) => ({
79+
payload: a * 2,
80+
meta: a / 2,
81+
error: true
82+
}))
83+
expect(actionCreator(10).error).toBe(true)
84+
})
85+
it('should use the meta returned from the prepareAction method', () => {
86+
const actionCreator = createAction('A_TYPE', (a: number) => ({
87+
payload: a * 2,
88+
meta: a / 2,
89+
error: true
90+
}))
91+
expect(actionCreator(10).meta).toBe(5)
92+
})
93+
})
94+
5195
describe('when passing a prepareAction that accepts multiple arguments', () => {
5296
it('should pass all arguments of the resulting actionCreator to prepareAction', () => {
5397
const actionCreator = createAction(

src/createAction.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,29 @@ import { IsUnknownOrNonInferrable } from './tsHelpers'
88
* @template P The type of the action's payload.
99
* @template T the type used for the action type.
1010
* @template M The type of the action's meta (optional)
11+
* @template E The type of the action's error (optional)
1112
*/
1213
export type PayloadAction<
1314
P = void,
1415
T extends string = string,
15-
M = void
16-
> = WithOptionalMeta<M, WithPayload<P, Action<T>>>
16+
M = void,
17+
E = void
18+
> = WithOptional<M, E, WithPayload<P, Action<T>>>
1719

1820
export type PrepareAction<P> =
1921
| ((...args: any[]) => { payload: P })
2022
| ((...args: any[]) => { payload: P; meta: any })
23+
| ((...args: any[]) => { payload: P; meta: any; error: any })
2124

2225
export type ActionCreatorWithPreparedPayload<
2326
PA extends PrepareAction<any> | void,
2427
T extends string = string
2528
> = WithTypeProperty<
2629
T,
2730
PA extends PrepareAction<infer P>
28-
? (...args: Parameters<PA>) => PayloadAction<P, T, MetaOrVoid<PA>>
31+
? (
32+
...args: Parameters<PA>
33+
) => PayloadAction<P, T, MetaOrVoid<PA>, ErrorOrVoid<PA>>
2934
: void
3035
>
3136

@@ -113,9 +118,13 @@ export function createAction(type: string, prepareAction?: Function) {
113118
if (!prepared) {
114119
throw new Error('prepareAction did not return an object')
115120
}
116-
return 'meta' in prepared
117-
? { type, payload: prepared.payload, meta: prepared.meta }
118-
: { type, payload: prepared.payload }
121+
122+
return {
123+
type,
124+
payload: prepared.payload,
125+
...('meta' in prepared && { meta: prepared.meta }),
126+
...('error' in prepared && { error: prepared.error })
127+
}
119128
}
120129
return { type, payload: args[0] }
121130
}
@@ -147,7 +156,9 @@ type Diff<T, U> = T extends U ? never : T
147156

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

150-
type WithOptionalMeta<M, T> = T & ([M] extends [void] ? {} : { meta: M })
159+
type WithOptional<M, E, T> = T &
160+
([M] extends [void] ? {} : { meta: M }) &
161+
([E] extends [void] ? {} : { error: E })
151162

152163
type WithTypeProperty<T, MergeIn> = {
153164
type: T
@@ -165,6 +176,12 @@ type MetaOrVoid<PA extends PrepareAction<any>> = ReturnType<PA> extends {
165176
? M
166177
: void
167178

179+
type ErrorOrVoid<PA extends PrepareAction<any>> = ReturnType<PA> extends {
180+
error: infer E
181+
}
182+
? E
183+
: void
184+
168185
type IfMaybeUndefined<P, True, False> = [undefined] extends [P] ? True : False
169186

170187
type IfVoid<P, True, False> = [void] extends [P] ? True : False

type-tests/files/createAction.typetest.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ function expectType<T>(p: T): T {
156156

157157
// typings:expect-error
158158
expectType<string>(strLenAction('test').payload)
159+
// typings:expect-error
160+
const error: any = strLenAction('test').error
159161
}
160162

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

172174
// typings:expect-error
173175
expectType<string>(strLenMetaAction('test').meta)
176+
// typings:expect-error
177+
const error: any = strLenMetaAction('test').error
178+
}
179+
180+
/*
181+
* Test: adding boolean error with prepareAction
182+
*/
183+
{
184+
const boolErrorAction = createAction('boolError', (payload: string) => ({
185+
payload,
186+
error: true
187+
}))
188+
189+
expectType<boolean>(boolErrorAction('test').error)
190+
191+
// typings:expect-error
192+
expectType<string>(boolErrorAction('test').error)
193+
}
194+
195+
/*
196+
* Test: adding string error with prepareAction
197+
*/
198+
{
199+
const strErrorAction = createAction('strError', (payload: string) => ({
200+
payload,
201+
error: 'this is an error'
202+
}))
203+
204+
expectType<string>(strErrorAction('test').error)
205+
206+
// typings:expect-error
207+
expectType<boolean>(strErrorAction('test').error)
174208
}
175209

176210
/*

0 commit comments

Comments
 (0)