Skip to content
This repository was archived by the owner on Feb 16, 2022. It is now read-only.

Commit 051880d

Browse files
Piotr Oleśpiotr-oles
authored andcommitted
feat: 🎸 Create more generic Detector<TState, TResult> type
Renamed Detector<S> to ActionsDetector<TState, TAction> in order to create more generic Detector<TState, TResult> type. Added ConditionDetector<TState> type. Added typings for TAction type (by default AnyAction). BREAKING CHANGE: 🧨 Renamed type Detector<S> to ActionsDetector<TState, TAction> and added Detector<TState, TResult> type (more generic).
1 parent bd99125 commit 051880d

File tree

8 files changed

+108
-57
lines changed

8 files changed

+108
-57
lines changed

src/Detector.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1+
import { Action, AnyAction } from "redux";
2+
13
/**
2-
* Function that compares previous and next state and can return action, array of actions or undefined.
4+
* Function that reduces previous and next state to single value.
35
*/
4-
export type Detector<S> = (prevState?: S, nextState?: S) => any | any[] | void;
6+
export type Detector<TState = any, TResult = any> = (
7+
prevState?: TState,
8+
nextState?: TState
9+
) => TResult;
10+
11+
/**
12+
* Function that compares previous and next state and can return action, array of actions or nothing.
13+
*/
14+
export type ActionsDetector<
15+
TState = any,
16+
TAction extends Action = AnyAction
17+
> = Detector<TState, TAction | TAction[] | void>;
18+
19+
/**
20+
* Function that compares previous and next state and returns boolean value
21+
*/
22+
export type ConditionDetector<TState = any> = Detector<TState, boolean>;

src/DetectorsMap.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/createDetectorEnhancer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Reducer, Store, StoreEnhancer } from "redux";
22
import { DetectableStore } from "./DetectableStore";
33
import { DetectableStoreExt } from "./DetectableStoreExt";
4-
import { Detector } from "./Detector";
4+
import { ActionsDetector } from "./Detector";
55

66
export const ActionTypes: { INIT: string } = {
77
INIT: "@@detector/INIT"
@@ -15,11 +15,11 @@ export type StoreDetectableEnhancer<S> = StoreEnhancer<
1515
/**
1616
* Creates detector enhancer that modifies redux store to use it with provided detector.
1717
*
18-
* @param detector Root detector
18+
* @param detector Root actions detector
1919
* @returns Store enhancer
2020
*/
2121
export function createDetectorEnhancer<S = any>(
22-
detector: Detector<S>
22+
detector: ActionsDetector<S>
2323
): StoreDetectableEnhancer<S> {
2424
if (typeof detector !== "function") {
2525
throw new Error("Expected the detector to be a function.");
@@ -43,7 +43,7 @@ export function createDetectorEnhancer<S = any>(
4343
const detectableStore: DetectableStore<any, any> = {
4444
...store,
4545
replaceDetector: function replaceDetector(
46-
nextDetector: Detector<any>
46+
nextDetector: ActionsDetector<any>
4747
): void {
4848
if (typeof nextDetector !== "function") {
4949
throw new Error("Expected the nextDetector to be a function.");

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export { DetectableStoreExt } from "./DetectableStoreExt";
66
// enhance redux store
77
export { createDetectorEnhancer } from "./createDetectorEnhancer";
88

9-
// reduce detectors
9+
// standard library
1010
export { reduceDetectors } from "./lib/reduceDetectors";
1111
export { combineDetectors } from "./lib/combineDetectors";
1212
export { mountDetector } from "./lib/mountDetector";

src/lib/combineDetectors.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { Detector } from "../Detector";
2-
import { DetectorsMap } from "../DetectorsMap";
1+
import { Action, AnyAction } from "redux";
2+
import { ActionsDetector } from "../Detector";
3+
4+
type ActionsDetectorsMap<
5+
TState extends object,
6+
TAction extends Action = AnyAction
7+
> = { [K in keyof TState]?: ActionsDetector<TState[K], TAction> };
38

49
/**
510
* Combine detectors to bind them to the local state.
@@ -8,17 +13,18 @@ import { DetectorsMap } from "../DetectorsMap";
813
* @param map Map of detectors bounded to state.
914
* @returns Combined detector
1015
*/
11-
export function combineDetectors<S extends object>(
12-
map: DetectorsMap<S>
13-
): Detector<S> {
16+
export function combineDetectors<
17+
TState extends object,
18+
TAction extends Action = AnyAction
19+
>(map: ActionsDetectorsMap<TState, TAction>): ActionsDetector<TState, TAction> {
1420
return function combinedDetector(
15-
prevState: S | undefined,
16-
nextState: S | undefined
21+
prevState?: TState,
22+
nextState?: TState
1723
): any[] {
1824
return Object.keys(map).reduce((reducedActions: any[], key: string) => {
19-
let actions: any | any[] | void = map[key as keyof S]!(
20-
prevState ? prevState[key as keyof S] : undefined,
21-
nextState ? nextState[key as keyof S] : undefined
25+
let actions: any | any[] | void = map[key as keyof TState]!(
26+
prevState ? prevState[key as keyof TState] : undefined,
27+
nextState ? nextState[key as keyof TState] : undefined
2228
);
2329

2430
if (actions) {

src/lib/mountDetector.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { Detector } from "../Detector";
22

3-
export function mountDetector<TOuterState, TState>(
4-
selector: (state: TOuterState | undefined) => TState,
5-
detector: Detector<TState>
6-
): Detector<TOuterState> {
3+
/**
4+
* Mounts detector to selected state using selectors. Works perfectly with reselect library.
5+
*/
6+
export function mountDetector<
7+
TOuterState = any,
8+
TInnerState = any,
9+
TResult = any
10+
>(
11+
selector: (state: TOuterState | undefined) => TInnerState,
12+
detector: Detector<TInnerState, TResult>
13+
): Detector<TOuterState, TResult> {
714
return function mountedDetector(
815
prevState: TOuterState | undefined,
916
nextState: TOuterState | undefined
10-
): any | any[] | void {
17+
): TResult {
1118
return detector(selector(prevState), selector(nextState));
1219
};
1320
}

src/lib/reduceDetectors.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { Detector } from "../Detector";
1+
import { Action, AnyAction } from "redux";
2+
import { ActionsDetector } from "../Detector";
23

3-
export function reduceDetectors<S>(...detectors: Detector<S>[]): Detector<S> {
4+
/**
5+
* Compose many action detectors into one detector that aggregates actions returned by given detectors
6+
*/
7+
export function reduceDetectors<TState, TAction extends Action = AnyAction>(
8+
...detectors: ActionsDetector<TState, TAction>[]
9+
): ActionsDetector<TState, TAction> {
410
// check detectors types in runtime
511
const invalidDetectorsIndexes: number[] = detectors
612
.map((detector, index) => (detector instanceof Function ? -1 : index))
@@ -19,11 +25,14 @@ export function reduceDetectors<S>(...detectors: Detector<S>[]): Detector<S> {
1925
}
2026

2127
return function reducedDetector(
22-
prevState: S | undefined,
23-
nextState: S | undefined
28+
prevState: TState | undefined,
29+
nextState: TState | undefined
2430
): any[] {
2531
return detectors
2632
.map(detector => detector(prevState, nextState) || [])
27-
.reduce((actions, nextActions) => actions.concat(nextActions), []);
33+
.reduce(
34+
(actions: TAction[], nextActions) => actions.concat(nextActions),
35+
[]
36+
);
2837
};
2938
}

test/reduceDetectors.spec.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
import { reduceDetectors } from "../src";
12

2-
import { reduceDetectors } from '../src';
3-
4-
describe('reduceDetectors', () => {
5-
it('should export reduceDetectors function', () => {
3+
describe("reduceDetectors", () => {
4+
it("should export composeDetectors function", () => {
65
expect(reduceDetectors).toBeInstanceOf(Function);
76
});
87

9-
it('should return valid detector for reduction of two detectors', () => {
8+
it("should return valid detector for composition of two detectors", () => {
109
function detectorA() {
11-
return [{type: 'ACTION_A'}, {type: 'ACTION_B'}];
10+
return [{ type: "ACTION_A" }, { type: "ACTION_B" }];
1211
}
1312

1413
function detectorB() {
15-
return [{type: 'ACTION_C'}];
14+
return [{ type: "ACTION_C" }];
1615
}
1716

1817
const detectorAB = reduceDetectors(detectorA, detectorB);
@@ -21,75 +20,90 @@ describe('reduceDetectors', () => {
2120
expect(detectorAB({}, {})).toBeInstanceOf(Array);
2221

2322
// we check it twice to be sure that detectorAB doesn't has any internal state.
24-
expect(detectorAB({}, {})).toEqual([{type: 'ACTION_A'}, {type: 'ACTION_B'}, {type: 'ACTION_C'}]);
25-
expect(detectorAB({}, {})).toEqual([{type: 'ACTION_A'}, {type: 'ACTION_B'}, {type: 'ACTION_C'}]);
23+
expect(detectorAB({}, {})).toEqual([
24+
{ type: "ACTION_A" },
25+
{ type: "ACTION_B" },
26+
{ type: "ACTION_C" }
27+
]);
28+
expect(detectorAB({}, {})).toEqual([
29+
{ type: "ACTION_A" },
30+
{ type: "ACTION_B" },
31+
{ type: "ACTION_C" }
32+
]);
2633
});
2734

28-
it('should pass states in valid order for reduction of two detectors', () => {
35+
it("should pass states in valid order for composition of two detectors", () => {
2936
function detectorA(prevState?: number, nextState?: number) {
3037
if (prevState && nextState && prevState > nextState) {
31-
return [{type: 'PREV_STATE_GREATER'}];
38+
return [{ type: "PREV_STATE_GREATER" }];
3239
}
3340

3441
return [];
3542
}
3643

3744
function detectorB(prevState?: number, nextState?: number) {
3845
if (prevState && nextState && nextState > prevState) {
39-
return [{type: 'NEXT_STATE_GREATER'}];
46+
return [{ type: "NEXT_STATE_GREATER" }];
4047
}
4148

4249
return [];
4350
}
4451

4552
const detectorAB = reduceDetectors(detectorA, detectorB);
4653

47-
expect(detectorAB(-10, 50)).toEqual([{type: 'NEXT_STATE_GREATER'}]);
48-
expect(detectorAB(30, 20)).toEqual([{type: 'PREV_STATE_GREATER'}]);
54+
expect(detectorAB(-10, 50)).toEqual([{ type: "NEXT_STATE_GREATER" }]);
55+
expect(detectorAB(30, 20)).toEqual([{ type: "PREV_STATE_GREATER" }]);
4956
});
5057

51-
it('should allow to reduce detectors with undefined result on no-action detect', () => {
58+
it("should allow to compose detectors with undefined result on no-action detect", () => {
5259
function detectorA(prevState?: number, nextState?: number) {
5360
if (prevState && nextState && prevState > nextState) {
54-
return [{type: 'PREV_STATE_GREATER'}];
61+
return [{ type: "PREV_STATE_GREATER" }];
5562
}
5663
}
5764

5865
function detectorB(prevState?: number, nextState?: number) {
5966
if (prevState && nextState && nextState > prevState) {
60-
return [{type: 'NEXT_STATE_GREATER'}];
67+
return [{ type: "NEXT_STATE_GREATER" }];
6168
}
6269
}
6370

6471
const detectorAB = reduceDetectors(detectorA, detectorB);
6572

66-
expect(detectorAB(-10, 50)).toEqual([{type: 'NEXT_STATE_GREATER'}]);
67-
expect(detectorAB(30, 20)).toEqual([{type: 'PREV_STATE_GREATER'}]);
73+
expect(detectorAB(-10, 50)).toEqual([{ type: "NEXT_STATE_GREATER" }]);
74+
expect(detectorAB(30, 20)).toEqual([{ type: "PREV_STATE_GREATER" }]);
6875
});
6976

70-
it('should allow to reduce detectors with array and single result', () => {
77+
it("should allow to compose detectors with array and single result", () => {
7178
function detectorA() {
72-
return [{type: 'ARRAY_DETECTOR'}];
79+
return [{ type: "ARRAY_DETECTOR" }];
7380
}
7481

7582
function detectorB() {
76-
return {type: 'SINGLE_DETECTOR'};
83+
return { type: "SINGLE_DETECTOR" };
7784
}
7885

7986
const detectorAB = reduceDetectors(detectorA, detectorB);
8087

81-
expect(detectorAB(undefined, undefined)).toEqual([{type: 'ARRAY_DETECTOR'}, {type: 'SINGLE_DETECTOR'}]);
88+
expect(detectorAB(undefined, undefined)).toEqual([
89+
{ type: "ARRAY_DETECTOR" },
90+
{ type: "SINGLE_DETECTOR" }
91+
]);
8292
});
8393

84-
it('should return valid reduced detector for empty arguments', () => {
94+
it("should return valid composed detector for empty arguments", () => {
8595
const emptyDetector = reduceDetectors();
8696

8797
expect(emptyDetector).toBeInstanceOf(Function);
8898
expect(emptyDetector(10, 20)).toEqual([]);
8999
});
90100

91-
it('should throw an exception for invalid arguments', () => {
92-
expect(() => { (reduceDetectors as any)({ 'foo': 'bar' }); }).toThrow();
93-
expect(() => { (reduceDetectors as any)(() => undefined, undefined); }).toThrow();
101+
it("should throw an exception for invalid arguments", () => {
102+
expect(() => {
103+
(reduceDetectors as any)({ foo: "bar" });
104+
}).toThrow();
105+
expect(() => {
106+
(reduceDetectors as any)(() => undefined, undefined);
107+
}).toThrow();
94108
});
95109
});

0 commit comments

Comments
 (0)