Skip to content

Commit 8d56a6f

Browse files
rkirovbrandonroberts
authored andcommitted
feat(effects): add smarter type inference for ofType operator. (#1183)
It is based on TS2.8 introduced conditional types. Upgrade package.json to that version. Also remove the deprecated non-pipable version of `ofType` so that we don't have to keep complicated types in two places. Fixes some tests post removal of non-pipeable version. Original implementation by @mtaran-google. BREAKING CHANGES: Removes .ofType method on Actions. Instead use the provided 'ofType' rxjs operator. BEFORE: ``` this.actions.ofType('INCREMENT') ``` AFTER: ``` import { ofType } from '@ngrx/store'; ... this.action.pipe(ofType('INCREMENT')) ```
1 parent 1448a0e commit 8d56a6f

File tree

4 files changed

+101
-29
lines changed

4 files changed

+101
-29
lines changed

modules/effects/spec/actions.spec.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@ import { hot, cold } from 'jasmine-marbles';
1111
import { of } from 'rxjs';
1212

1313
describe('Actions', function() {
14-
let actions$: Actions;
14+
let actions$: Actions<AddAction | SubtractAction>;
1515
let dispatcher: ScannedActionsSubject;
1616

1717
const ADD = 'ADD';
1818
const SUBTRACT = 'SUBTRACT';
1919

20+
interface AddAction extends Action {
21+
type: 'ADD';
22+
}
23+
24+
interface SubtractAction extends Action {
25+
type: 'SUBTRACT';
26+
}
27+
2028
function reducer(state: number = 0, action: Action) {
2129
switch (action.type) {
2230
case ADD:
@@ -58,10 +66,10 @@ describe('Actions', function() {
5866
actions.forEach(action => dispatcher.next(action));
5967
});
6068

61-
it('should let you filter out actions', function() {
62-
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
63-
const expected = actions.filter(type => type === ADD);
69+
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
70+
const expected = actions.filter(type => type === ADD);
6471

72+
it('should let you filter out actions', function() {
6573
actions$
6674
.pipe(
6775
ofType(ADD),
@@ -78,16 +86,20 @@ describe('Actions', function() {
7886
dispatcher.complete();
7987
});
8088

81-
it('should support using the ofType instance operator', () => {
82-
const action = { type: ADD };
83-
84-
const response = cold('-b', { b: true });
85-
const expected = cold('--c', { c: true });
86-
87-
const effect$ = new Actions(hot('-a', { a: action }))
88-
.ofType(ADD)
89-
.pipe(switchMap(() => response));
89+
it('should let you filter out actions and ofType can take an explicit type argument', function() {
90+
actions$
91+
.pipe(
92+
ofType<AddAction>(ADD),
93+
map(update => update.type),
94+
toArray()
95+
)
96+
.subscribe({
97+
next(actual) {
98+
expect(actual).toEqual(expected);
99+
},
100+
});
90101

91-
expect(effect$).toBeObservable(expected);
102+
actions.forEach(action => dispatcher.next({ type: action }));
103+
dispatcher.complete();
92104
});
93105
});

modules/effects/spec/effects_feature_module.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ class FeatureEffects {
171171
);
172172

173173
@Effect()
174-
effectWithStore = this.actions.ofType('INCREMENT').pipe(
174+
effectWithStore = this.actions.pipe(
175+
ofType('INCREMENT'),
175176
withLatestFrom(this.store.select(getDataState)),
176177
map(([action, state]) => ({ type: 'INCREASE' }))
177178
);

modules/effects/src/actions.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Inject, Injectable } from '@angular/core';
22
import { Action, ScannedActionsSubject } from '@ngrx/store';
3-
import { Observable, Operator, OperatorFunction } from 'rxjs';
3+
import { Observable, OperatorFunction, Operator } from 'rxjs';
44
import { filter } from 'rxjs/operators';
55

66
@Injectable()
@@ -19,20 +19,79 @@ export class Actions<V = Action> extends Observable<V> {
1919
observable.operator = operator;
2020
return observable;
2121
}
22-
23-
/**
24-
* @deprecated from 6.1.0. Use the pipeable `ofType` operator instead.
25-
*/
26-
ofType<V2 extends V = V>(...allowedTypes: string[]): Actions<V2> {
27-
return ofType<any>(...allowedTypes)(this as Actions<any>) as Actions<V2>;
28-
}
2922
}
3023

31-
export function ofType<T extends Action>(
24+
/**
25+
* 'ofType' filters an Observable of Actions into an observable of the actions
26+
* whose type strings are passed to it.
27+
*
28+
* For example, `actions.pipe(ofType('add'))` returns an
29+
* `Observable<AddtionAction>`
30+
*
31+
* Properly typing this function is hard and requires some advanced TS tricks
32+
* below.
33+
*
34+
* Type narrowing automatically works, as long as your `actions` object
35+
* starts with a `Actions<SomeUnionOfActions>` instead of generic `Actions`.
36+
*
37+
* For backwards compatibility, when one passes a single type argument
38+
* `ofType<T>('something')` the result is an `Observable<T>`. Note, that `T`
39+
* completely overrides any possible inference from 'something'.
40+
*
41+
* Unfortunately, for unknown 'actions: Actions' these types will produce
42+
* 'Observable<never>'. In such cases one has to manually set the generic type
43+
* like `actions.ofType<AdditionAction>('add')`.
44+
*/
45+
export function ofType<
46+
V extends Extract<U, { type: T1 }>,
47+
T1 extends string = string,
48+
U extends Action = Action
49+
>(t1: T1): OperatorFunction<U, V>;
50+
export function ofType<
51+
V extends Extract<U, { type: T1 | T2 }>,
52+
T1 extends string = string,
53+
T2 extends string = string,
54+
U extends Action = Action
55+
>(t1: T1, t2: T2): OperatorFunction<U, V>;
56+
export function ofType<
57+
V extends Extract<U, { type: T1 | T2 | T3 }>,
58+
T1 extends string = string,
59+
T2 extends string = string,
60+
T3 extends string = string,
61+
U extends Action = Action
62+
>(t1: T1, t2: T2, t3: T3): OperatorFunction<U, V>;
63+
export function ofType<
64+
V extends Extract<U, { type: T1 | T2 | T3 | T4 }>,
65+
T1 extends string = string,
66+
T2 extends string = string,
67+
T3 extends string = string,
68+
T4 extends string = string,
69+
U extends Action = Action
70+
>(t1: T1, t2: T2, t3: T3, t4: T4): OperatorFunction<U, V>;
71+
export function ofType<
72+
V extends Extract<U, { type: T1 | T2 | T3 | T4 | T5 }>,
73+
T1 extends string = string,
74+
T2 extends string = string,
75+
T3 extends string = string,
76+
T4 extends string = string,
77+
T5 extends string = string,
78+
U extends Action = Action
79+
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): OperatorFunction<U, V>;
80+
/**
81+
* Fallback for more than 5 arguments.
82+
* There is no inference, so the return type is the same as the input -
83+
* Observable<Action>.
84+
*
85+
* We provide a type parameter, even though TS will not infer it from the
86+
* arguments, to preserve backwards compatibility with old versions of ngrx.
87+
*/
88+
export function ofType<V extends Action>(
89+
...allowedTypes: string[]
90+
): OperatorFunction<Action, V>;
91+
export function ofType(
3292
...allowedTypes: string[]
33-
): OperatorFunction<Action, T> {
34-
return filter(
35-
(action: Action): action is T =>
36-
allowedTypes.some(type => type === action.type)
93+
): OperatorFunction<Action, Action> {
94+
return filter((action: Action) =>
95+
allowedTypes.some(type => type === action.type)
3796
);
3897
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,4 @@
152152
"url": "https://opencollective.com/ngrx",
153153
"logo": "https://opencollective.com/opencollective/logo.txt"
154154
}
155-
}
155+
}

0 commit comments

Comments
 (0)