Skip to content

Commit 100970b

Browse files
feat(effects): catch action creators being returned in effect without being called (#2536)
1 parent 6caae70 commit 100970b

File tree

6 files changed

+47
-10
lines changed

6 files changed

+47
-10
lines changed

modules/effects/spec/effect_sources.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ describe('EffectSources', () => {
375375
}
376376

377377
class SourceError {
378-
e$ = createEffect(() => throwError(error));
378+
e$ = createEffect(() => throwError(error) as any);
379379
}
380380

381381
class SourceH {

modules/effects/spec/effects_error_handler.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('Effects Error Handler', () => {
9393
}
9494

9595
class AlwaysErrorEffect {
96-
effect$ = createEffect(() => throwError('always an error'));
96+
effect$ = createEffect(() => throwError('always an error') as any);
9797
}
9898

9999
/**

modules/effects/spec/types/effect_creator.spec.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe('createEffect()', () => {
66
code => `
77
import { Action } from '@ngrx/store';
88
import { createEffect } from '@ngrx/effects';
9+
import { createAction } from '@ngrx/store';
910
import { of } from 'rxjs';
1011
1112
${code}`,
@@ -21,7 +22,23 @@ describe('createEffect()', () => {
2122
expectSnippet(`
2223
const effect = createEffect(() => of({ foo: 'a' }));
2324
`).toFail(
24-
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
25+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'EffectResult<Action>'./
26+
);
27+
});
28+
29+
it('should help with action creator that is not called', () => {
30+
// Action creator is called with parentheses.
31+
expectSnippet(`
32+
const action = createAction('action without props');
33+
const effect = createEffect(() => of(action()));
34+
`).toSucceed();
35+
36+
// Action creator is not called (no parentheses).
37+
expectSnippet(`
38+
const action = createAction('action without props');
39+
const effect = createEffect(() => of(action));
40+
`).toFail(
41+
/ActionCreator cannot be dispatched. Did you forget to call the action creator function/
2542
);
2643
});
2744

@@ -33,7 +50,7 @@ describe('createEffect()', () => {
3350
expectSnippet(`
3451
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
3552
`).toFail(
36-
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
53+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'EffectResult<Action>'./
3754
);
3855
});
3956
});
@@ -47,8 +64,16 @@ describe('createEffect()', () => {
4764
expectSnippet(`
4865
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
4966
`).toFail(
50-
/Type '{ foo: string; }' is not assignable to type 'Observable<unknown> | ((...args: any[]) => Observable<unknown>)'./
67+
/Type '{ foo: string; }' is not assignable to type 'EffectResult<unknown>'./
5168
);
5269
});
70+
71+
it('should allow action creator even if it is not called', () => {
72+
// Action creator is not called (no parentheses), but we have no-dispatch.
73+
expectSnippet(`
74+
const action = createAction('action without props');
75+
const effect = createEffect(() => of(action), { dispatch: false });
76+
`).toSucceed();
77+
});
5378
});
5479
});

modules/effects/src/effect_creator.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Observable } from 'rxjs';
2-
import { Action } from '@ngrx/store';
2+
import { Action, ActionCreator } from '@ngrx/store';
33
import {
44
EffectMetadata,
55
EffectConfig,
@@ -10,6 +10,15 @@ import {
1010

1111
type DispatchType<T> = T extends { dispatch: infer U } ? U : true;
1212
type ObservableType<T, OriginalType> = T extends false ? OriginalType : Action;
13+
type EffectResult<OT> = Observable<OT> | ((...args: any[]) => Observable<OT>);
14+
type ConditionallyDisallowActionCreator<DT, Result> = DT extends false
15+
? unknown // If DT (DispatchType is false, then we don't enforce any return types)
16+
: Result extends EffectResult<infer OT>
17+
? OT extends ActionCreator
18+
? 'ActionCreator cannot be dispatched. Did you forget to call the action creator function?'
19+
: unknown
20+
: unknown;
21+
1322
/**
1423
* @description
1524
* Creates an effect from an `Observable` and an `EffectConfig`.
@@ -46,8 +55,11 @@ export function createEffect<
4655
C extends EffectConfig,
4756
DT extends DispatchType<C>,
4857
OT extends ObservableType<DT, OT>,
49-
R extends Observable<OT> | ((...args: any[]) => Observable<OT>)
50-
>(source: () => R, config?: Partial<C>): R & CreateEffectMetadata {
58+
R extends EffectResult<OT>
59+
>(
60+
source: () => R & ConditionallyDisallowActionCreator<DT, R>,
61+
config?: Partial<C>
62+
): R & CreateEffectMetadata {
5163
const effect = source();
5264
const value: EffectConfig = {
5365
...DEFAULT_EFFECT_CONFIG,

modules/store/spec/types/store.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('Store', () => {
1616

1717
it('should not allow passing action creator function without calling it', () => {
1818
expectSnippet(`store.dispatch(fooAction);`).toFail(
19-
/is not assignable to type '"Functions are not allowed to be dispatched. Did you forget to call action creator function/
19+
/is not assignable to type '"Functions are not allowed to be dispatched. Did you forget to call the action creator function/
2020
);
2121
});
2222
});

modules/store/src/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class Store<T = object> extends Observable<T>
9898
action: V &
9999
FunctionIsNotAllowed<
100100
V,
101-
'Functions are not allowed to be dispatched. Did you forget to call action creator function?'
101+
'Functions are not allowed to be dispatched. Did you forget to call the action creator function?'
102102
>
103103
) {
104104
this.actionsObserver.next(action);

0 commit comments

Comments
 (0)