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

feat(switchReduce): Added type safety way to reduce state #416

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, switchReduce } 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 = 'SUBTRACT';
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);
}