Skip to content

Commit

Permalink
Add smarter type inference for ofType pipe.
Browse files Browse the repository at this point in the history
It is based on TS2.8 introduced conditional types. Upgrade package.json
to that version.

Move previous version of actions.ts to @ngrx/effects/compat module.

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-pipable version.

Original implementation by @mtaran-google.
  • Loading branch information
rkirov committed Jul 13, 2018
1 parent 2cb81d8 commit d04cae3
Show file tree
Hide file tree
Showing 11 changed files with 17,323 additions and 29 deletions.
19 changes: 19 additions & 0 deletions modules/effects/compat/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package(default_visibility = ["//visibility:public"])

exports_files(["package.json"])

load("//tools:defaults.bzl", "ng_module")

ng_module(
name = "compat",
srcs = glob([
"src/actions.ts",
]),
module_name = "@ngrx/effects/compat",
visibility = ["//visibility:public"],
deps = [
"//modules/store",
"@rxjs",
"@rxjs//operators",
],
)
3 changes: 3 additions & 0 deletions modules/effects/compat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@ngrx/effects/compat"
}
12 changes: 12 additions & 0 deletions modules/effects/compat/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
entry: './dist/effects/@ngrx/effects/compat.es5.js',
dest: './dist/effects/bundles/effects-compat.umd.js',
format: 'umd',
exports: 'named',
moduleName: 'ngrx.effects.compat',
globals: {
'@angular/core': 'ng.core',
'@ngrx/effects': 'ngrx.effects',
'rxjs': 'Rx',
}
}
34 changes: 34 additions & 0 deletions modules/effects/compat/src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Inject, Injectable } from '@angular/core';
import { Action, ScannedActionsSubject } from '@ngrx/store';
import { Observable, Operator, OperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

@Injectable()
export class Actions<V = Action> extends Observable<V> {
constructor(@Inject(ScannedActionsSubject) source?: Observable<V>) {
super();

if (source) {
this.source = source;
}
}

lift<R>(operator: Operator<V, R>): Observable<R> {
const observable = new Actions<R>();
observable.source = this;
observable.operator = operator;
return observable;
}

ofType<V2 extends V = V>(...allowedTypes: string[]): Actions<V2> {
return ofType<any>(...allowedTypes)(this as Actions<any>) as Actions<V2>;
}
}

export function ofType<T extends Action>(
...allowedTypes: string[]
): OperatorFunction<Action, T> {
return filter((action: Action): action is T =>
allowedTypes.some(type => type === action.type)
);
}
14 changes: 14 additions & 0 deletions modules/effects/compat/tsconfig-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig-build",
"compilerOptions": {
"paths": {
"@ngrx/store": ["../../dist/packages/store"],
"@ngrx/effects": ["../../dist/packages/effects"]
}
},
"files": ["index.ts"],
"angularCompilerOptions": {
// Work around for issue: https://github.com/angular/angular/issues/22210
"strictMetadataEmit": false
}
}
34 changes: 21 additions & 13 deletions modules/effects/spec/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ describe('Actions', function() {
const ADD = 'ADD';
const SUBTRACT = 'SUBTRACT';

interface AddAcction extends Action {
type: 'ADD';
}

interface SubtractAction extends Action {
type: 'SUBTRACT';
}

function reducer(state: number = 0, action: Action) {
switch (action.type) {
case ADD:
Expand Down Expand Up @@ -58,10 +66,10 @@ describe('Actions', function() {
actions.forEach(action => dispatcher.next(action));
});

it('should let you filter out actions', function() {
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
const expected = actions.filter(type => type === ADD);
const actions = [ADD, ADD, SUBTRACT, ADD, SUBTRACT];
const expected = actions.filter(type => type === ADD);

it('should let you filter out actions', function() {
actions$
.pipe(ofType(ADD), map(update => update.type), toArray())
.subscribe({
Expand All @@ -74,16 +82,16 @@ describe('Actions', function() {
dispatcher.complete();
});

it('should support using the ofType instance operator', () => {
const action = { type: ADD };

const response = cold('-b', { b: true });
const expected = cold('--c', { c: true });

const effect$ = new Actions(hot('-a', { a: action }))
.ofType(ADD)
.pipe(switchMap(() => response));
it('should let you filter out actions and ofType can take an explicit type argument', function() {
actions$
.pipe(ofType<AddAcction>(ADD), map(update => update.type), toArray())
.subscribe({
next(actual) {
expect(actual).toEqual(expected);
},
});

expect(effect$).toBeObservable(expected);
actions.forEach(action => dispatcher.next({ type: action }));
dispatcher.complete();
});
});
13 changes: 6 additions & 7 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { Observable } from 'rxjs';
import { map, withLatestFrom } from 'rxjs/operators';

import { Actions, Effect, EffectsModule } from '../';
import { Actions, Effect, EffectsModule, ofType } from '../';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { FEATURE_EFFECTS } from '../src/tokens';
Expand Down Expand Up @@ -118,12 +118,11 @@ class FeatureEffects {
constructor(private actions: Actions, private store: Store<State>) {}

@Effect()
effectWithStore = this.actions
.ofType('INCREMENT')
.pipe(
withLatestFrom(this.store.select(getDataState)),
map(([action, state]) => ({ type: 'INCREASE' }))
);
effectWithStore = this.actions.pipe(
ofType('INCREMENT'),
withLatestFrom(this.store.select(getDataState)),
map(([action, state]) => ({ type: 'INCREASE' }))
);
}

@NgModule({
Expand Down
52 changes: 44 additions & 8 deletions modules/effects/src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Inject, Injectable } from '@angular/core';
import { Action, ScannedActionsSubject } from '@ngrx/store';
import { Observable, Operator, OperatorFunction } from 'rxjs';
import { Observable, OperatorFunction, Operator } from 'rxjs';
import { filter } from 'rxjs/operators';

// Removes types from T that are not assignable to U.
export type Filter<T, U> = T extends U ? T : never;

@Injectable()
export class Actions<V = Action> extends Observable<V> {
constructor(@Inject(ScannedActionsSubject) source?: Observable<V>) {
Expand All @@ -19,16 +22,49 @@ export class Actions<V = Action> extends Observable<V> {
observable.operator = operator;
return observable;
}

ofType<V2 extends V = V>(...allowedTypes: string[]): Actions<V2> {
return ofType<any>(...allowedTypes)(this as Actions<any>) as Actions<V2>;
}
}

export function ofType<T extends Action>(
/**
* Type propagation currently scales only to five input strings.
*/
export function ofType<
V extends Filter<U, { type: T1 }>,
T1 extends string = string,
U extends Action = Action
>(t1: T1): OperatorFunction<U, V>;
export function ofType<
V extends Filter<U, { type: T1 | T2 }>,
T1 extends string = string,
T2 extends string = string,
U extends Action = Action
>(t1: T1, t2: T2): OperatorFunction<U, V>;
export function ofType<
V extends Filter<U, { type: T1 | T2 | T3 }>,
T1 extends string = string,
T2 extends string = string,
U extends Action = Action
>(t1: T1, t2: T2, t3: T3): OperatorFunction<U, V>;
export function ofType<
V extends Filter<U, { type: T1 | T2 | T3 | T4 }>,
T1 extends string = string,
T2 extends string = string,
T3 extends string = string,
T4 extends string = string,
U extends Action = Action
>(t1: T1, t2: T2, t3: T3, t4: T4): OperatorFunction<U, V>;
export function ofType<
V extends Filter<U, { type: T1 | T2 | T3 | T4 | T5 }>,
T1 extends string = string,
T2 extends string = string,
T3 extends string = string,
T4 extends string = string,
T5 extends string = string,
U extends Action = Action
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): OperatorFunction<U, V>;
export function ofType<U extends Action>(
...allowedTypes: string[]
): OperatorFunction<Action, T> {
return filter((action: Action): action is T =>
): OperatorFunction<U, U> {
return filter((action: Action): action is U =>
allowedTypes.some(type => type === action.type)
);
}
Loading

0 comments on commit d04cae3

Please sign in to comment.