Skip to content
This repository was archived by the owner on Jan 10, 2018. It is now read-only.

Commit 5d8b739

Browse files
committed
feat(switchReduce): Added type safety way to reduce state
1 parent 49e7168 commit 5d8b739

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,40 @@ class MyAppComponent {
134134
}
135135
```
136136

137+
Using typed actions in conjunction with `switchReduce` allow you to write reducers in a more type safety way.
138+
139+
```ts
140+
// counter.ts
141+
import { ActionReducer, TypedAction, switchReduce } from '@ngrx/store';
142+
143+
export class AddAction implements TypedAction<number> {
144+
readonly type = 'ADD';
145+
constructor(public readonly payload: number) {}
146+
}
147+
export class SubtractAction implements TypedAction<number> {
148+
readonly type = 'SUBTRACT';
149+
constructor(public readonly payload: number) {}
150+
}
151+
export class ResetAction implements TypedAction<any> {
152+
readonly type = 'RESET';
153+
readonly payload: any;
154+
constructor() {}
155+
}
156+
157+
export const counterReducer: ActionReducer<number> =
158+
(state: number = 0, action: TypedAction<any>) =>
159+
switchReduce(state, action)
160+
.byClass(AddAction, (num: number) => {
161+
return state + num;
162+
})
163+
.byClass(SubtractAction, (num: number) => {
164+
return state - num;
165+
})
166+
.byClass(ResetAction, () => {
167+
return 0;
168+
})
169+
.reduce();
170+
```
137171

138172
## Contributing
139173
Please read [contributing guidelines here](https://github.com/ngrx/store/blob/master/CONTRIBUTING.md).

spec/switch-reduce.spec.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {switchReduce} from '../src/utils';
2+
import {TypedAction} from '../src/dispatcher';
3+
interface TestState {
4+
num: number;
5+
}
6+
7+
class AddNumberAction implements TypedAction<number> {
8+
type: 'ADD_NUMBER';
9+
constructor(public payload: number) {}
10+
}
11+
12+
class SubtractNumberAction implements TypedAction<number> {
13+
type: 'SUBTRACT_NUMBER';
14+
constructor(public payload: number) {}
15+
}
16+
17+
let testState: TestState;
18+
describe('switchReduce', () => {
19+
beforeEach(() => {
20+
testState = {
21+
num: 1
22+
};
23+
});
24+
25+
it('should return initial state with no cases and no default', () => {
26+
const runSpy = jasmine.createSpy('spy');
27+
const payload = 1;
28+
29+
switchReduce(testState, new AddNumberAction(payload)).reduce();
30+
31+
expect(runSpy).not.toHaveBeenCalled();
32+
});
33+
34+
it('should take default if nothing else specified', () => {
35+
const newState = switchReduce(testState, new AddNumberAction(1))
36+
.reduce(() => ({
37+
num: 5
38+
}));
39+
40+
expect(newState.num).toBe(5);
41+
});
42+
43+
it('should take default if nothing else matches', () => {
44+
const runSpy = jasmine.createSpy('spy');
45+
46+
const newState = switchReduce(testState, new AddNumberAction(1))
47+
.byClass(SubtractNumberAction, runSpy)
48+
.byType('NOT_EXISTING', runSpy)
49+
.reduce(() => ({
50+
num: 5
51+
}));
52+
53+
expect(newState.num).toBe(5);
54+
expect(runSpy).not.toHaveBeenCalled();
55+
});
56+
57+
it('should execute run function only once', () => {
58+
const runSpy = jasmine.createSpy('spy');
59+
const payload = 1;
60+
61+
switchReduce(testState, new AddNumberAction(payload))
62+
.byClass(AddNumberAction, runSpy)
63+
.byClasses([AddNumberAction, SubtractNumberAction], runSpy)
64+
.byType('ADD_NUMBER', runSpy)
65+
.byTypes(['ADD_NUMBER', 'SUBTRACT_NUMBER'], runSpy)
66+
.reduce(runSpy);
67+
68+
expect(runSpy).toHaveBeenCalledWith(payload, jasmine.any(AddNumberAction), jasmine.anything());
69+
expect(runSpy.calls.count()).toBe(1);
70+
});
71+
72+
it('should execute same byClasses for each action', () => {
73+
const applySwitchReduce = (state: TestState, action: TypedAction<number>) =>
74+
switchReduce(state, action)
75+
.byClasses([AddNumberAction, SubtractNumberAction], (payload: number, innerAction: TypedAction<number>) => {
76+
const addend: number = innerAction instanceof AddNumberAction ? payload : -payload;
77+
return Object.assign({}, state, {
78+
num: state.num + addend
79+
});
80+
})
81+
.reduce();
82+
83+
const newState1 = applySwitchReduce(testState, new AddNumberAction(1));
84+
const newState2 = applySwitchReduce(newState1, new SubtractNumberAction(2));
85+
expect(newState1.num).toBe(2);
86+
expect(newState2.num).toBe(0);
87+
});
88+
});

src/dispatcher.ts

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export interface Action {
55
payload?: any;
66
}
77

8+
export interface TypedAction<T> extends Action {
9+
payload: T;
10+
}
11+
812
export class Dispatcher extends BehaviorSubject<Action> {
913
static INIT = '@ngrx/store/init';
1014

src/utils.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {ActionReducer} from './reducer';
2+
import {TypedAction} from './dispatcher';
23

34
export function combineReducers(reducers: any): ActionReducer<any> {
45
const reducerKeys = Object.keys(reducers);
@@ -28,3 +29,50 @@ export function combineReducers(reducers: any): ActionReducer<any> {
2829
return hasChanged ? nextState : state;
2930
};
3031
}
32+
33+
export type SwitchReduceRun<P, S> = (payload: P, action?: TypedAction<P>, state?: S) => S;
34+
35+
export class SwitchReduceBuilder<S> {
36+
private newState: S;
37+
38+
constructor(private state: S, private action: TypedAction<any>) {
39+
this.newState = state;
40+
}
41+
42+
byClass<P>(actionConstructor: {new(P): TypedAction<P>; }, run: SwitchReduceRun<P, S>): SwitchReduceBuilder<S> {
43+
if (this.newState === this.state && this.action instanceof actionConstructor) {
44+
this.newState = run(this.action.payload, this.action, this.state);
45+
}
46+
return this;
47+
}
48+
49+
byClasses(actionConstructors: {new(a: any): TypedAction<any>; }[], run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
50+
actionConstructors.forEach((actionConstructor) =>
51+
this.byClass(actionConstructor, run));
52+
return this;
53+
}
54+
55+
byType(type: string, run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
56+
if (this.newState === this.state && type === this.action.type) {
57+
this.newState = run(this.action.payload, this.action, this.state);
58+
}
59+
return this;
60+
}
61+
62+
byTypes(types: string[], run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
63+
types.forEach((type) =>
64+
this.byType(type, run));
65+
return this;
66+
}
67+
68+
reduce(defaultRun?: SwitchReduceRun<any, S>): S {
69+
if (defaultRun instanceof Function && this.newState === this.state) {
70+
this.newState = defaultRun(this.action.payload, null, this.state);
71+
}
72+
return this.newState;
73+
}
74+
}
75+
76+
export function switchReduce<S>(state: S, action: TypedAction<any>): SwitchReduceBuilder<S> {
77+
return new SwitchReduceBuilder(state, action);
78+
}

0 commit comments

Comments
 (0)