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

Commit

Permalink
feat(switchReduce): Added type safty way to reduce state
Browse files Browse the repository at this point in the history
  • Loading branch information
Gion Kunz committed May 14, 2017
1 parent 49e7168 commit b4f28aa
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,40 @@ class MyAppComponent {
}
```

Using typed actions in conjunction with `switchReduce` allow you to write reducers in a more type safety way.

```ts
// counter.ts
import { ActionReducer, TypedAction } from '@ngrx/store';

export class AddAction implements TypedAction<number> {
readonly type = 'ADD';
constructor(public readonly payload: number) {}
}
export class SubtractAction implements TypedAction<number> {
readonly type = 'ADD';
constructor(public readonly payload: number) {}
}
export class ResetAction implements TypedAction<any> {
readonly type = 'RESET';
readonly payload: any;
constructor() {}
}

export const counterReducer: ActionReducer<number> =
(state: number = 0, action: TypedAction<any>) =>
switchReduce(state, action)
.byClass(AddAction, (num: number) => {
return state + num;
})
.byClass(SubtractAction, (num: number) => {
return state - num;
})
.byClass(ResetAction, () => {
return 0;
})
.reduce();
```

## Contributing
Please read [contributing guidelines here](https://github.com/ngrx/store/blob/master/CONTRIBUTING.md).
88 changes: 88 additions & 0 deletions spec/switch-reduce.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {switchReduce} from '../src/utils';
import {TypedAction} from '../src/dispatcher';
interface TestState {
num: number;
}

class AddNumberAction implements TypedAction<number> {
type: 'ADD_NUMBER';
constructor(public payload: number) {}
}

class SubtractNumberAction implements TypedAction<number> {
type: 'SUBTRACT_NUMBER';
constructor(public payload: number) {}
}

let testState: TestState;
describe('switchReduce', () => {
beforeEach(() => {
testState = {
num: 1
};
});

it('should return initial state with no cases and no default', () => {
const runSpy = jasmine.createSpy('spy');
const payload = 1;

switchReduce(testState, new AddNumberAction(payload)).reduce();

expect(runSpy).not.toHaveBeenCalled();
});

it('should take default if nothing else specified', () => {
const newState = switchReduce(testState, new AddNumberAction(1))
.reduce(() => ({
num: 5
}));

expect(newState.num).toBe(5);
});

it('should take default if nothing else matches', () => {
const runSpy = jasmine.createSpy('spy');

const newState = switchReduce(testState, new AddNumberAction(1))
.byClass(SubtractNumberAction, runSpy)
.byType('NOT_EXISTING', runSpy)
.reduce(() => ({
num: 5
}));

expect(newState.num).toBe(5);
expect(runSpy).not.toHaveBeenCalled();
});

it('should execute run function only once', () => {
const runSpy = jasmine.createSpy('spy');
const payload = 1;

switchReduce(testState, new AddNumberAction(payload))
.byClass(AddNumberAction, runSpy)
.byClasses([AddNumberAction, SubtractNumberAction], runSpy)
.byType('ADD_NUMBER', runSpy)
.byTypes(['ADD_NUMBER', 'SUBTRACT_NUMBER'], runSpy)
.reduce(runSpy);

expect(runSpy).toHaveBeenCalledWith(payload, jasmine.any(AddNumberAction), jasmine.anything());
expect(runSpy.calls.count()).toBe(1);
});

it('should execute same byClasses for each action', () => {
const applySwitchReduce = (state: TestState, action: TypedAction<number>) =>
switchReduce(state, action)
.byClasses([AddNumberAction, SubtractNumberAction], (payload: number, innerAction: TypedAction<number>) => {
const addend: number = innerAction instanceof AddNumberAction ? payload : -payload;
return Object.assign({}, state, {
num: state.num + addend
});
})
.reduce();

const newState1 = applySwitchReduce(testState, new AddNumberAction(1));
const newState2 = applySwitchReduce(newState1, new SubtractNumberAction(2));
expect(newState1.num).toBe(2);
expect(newState2.num).toBe(0);
});
});
4 changes: 4 additions & 0 deletions src/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export interface Action {
payload?: any;
}

export interface TypedAction<T> extends Action {
payload: T;
}

export class Dispatcher extends BehaviorSubject<Action> {
static INIT = '@ngrx/store/init';

Expand Down
48 changes: 48 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {ActionReducer} from './reducer';
import {TypedAction} from './dispatcher';

export function combineReducers(reducers: any): ActionReducer<any> {
const reducerKeys = Object.keys(reducers);
Expand Down Expand Up @@ -28,3 +29,50 @@ export function combineReducers(reducers: any): ActionReducer<any> {
return hasChanged ? nextState : state;
};
}

export type SwitchReduceRun<P, S> = (payload: P, action?: TypedAction<P>, state?: S) => S;

export class SwitchReduceBuilder<S> {
private newState: S;

constructor(private state: S, private action: TypedAction<any>) {
this.newState = state;
}

byClass<P>(actionConstructor: {new(P): TypedAction<P>; }, run: SwitchReduceRun<P, S>): SwitchReduceBuilder<S> {
if (this.newState === this.state && this.action instanceof actionConstructor) {
this.newState = run(this.action.payload, this.action, this.state);
}
return this;
}

byClasses(actionConstructors: {new(a: any): TypedAction<any>; }[], run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
actionConstructors.forEach((actionConstructor) =>
this.byClass(actionConstructor, run));
return this;
}

byType(type: string, run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
if (this.newState === this.state && type === this.action.type) {
this.newState = run(this.action.payload, this.action, this.state);
}
return this;
}

byTypes(types: string[], run: SwitchReduceRun<any, S>): SwitchReduceBuilder<S> {
types.forEach((type) =>
this.byType(type, run));
return this;
}

reduce(defaultRun?: SwitchReduceRun<any, S>): S {
if (defaultRun instanceof Function && this.newState === this.state) {
this.newState = defaultRun(this.action.payload, null, this.state);
}
return this.newState;
}
}

export function switchReduce<S>(state: S, action: TypedAction<any>): SwitchReduceBuilder<S> {
return new SwitchReduceBuilder(state, action);
}

0 comments on commit b4f28aa

Please sign in to comment.