Skip to content

Commit 91c5eec

Browse files
Added tassign, docs about typing reducers (#221)
* Added tassign, docs about typing reducers Fixes #216 * Linter fix * Typo fix * Corrections and clarifications * Update strongly-typed-reducers.md * Update strongly-typed-reducers.md * unit tests tor tassign * Code review change: split tassign into its own package. [fixes #216] * minor corrections
1 parent b519883 commit 91c5eec

File tree

6 files changed

+177
-4
lines changed

6 files changed

+177
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,4 @@ We also have a number of 'cookbooks' for specific Angular 2 topics:
154154
* [Managing Side-Effects with redux-observable Epics](docs/epics.md)
155155
* [Using the Redux DevTools Chrome Extension](docs/redux-dev-tools.md)
156156
* [Ng2Redux and ImmutableJS](docs/immutable-js.md)
157+
* [Strongy Typed Reducers](docs/strongly-typed-reducers.md)

docs/strongly-typed-reducers.md

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Strongly Typed Reducers
2+
3+
It's good practice in typescript to be as specific about your types as possible.
4+
This helps you catch errors at compile-time instead of run-time.
5+
6+
Reducers are no exception to this rule. However it's not always obvious how to
7+
make this happen in practice.
8+
9+
## Reducer Typing Best Practices
10+
11+
### Define an Interface for your State
12+
13+
It's important to strongly type the data in your store, and this is done by
14+
defining types for the `state` arguments to your reducers:
15+
16+
```typescript
17+
export type TFoo: string;
18+
19+
// Being explicit about the state argument and return types ensures that all your
20+
// reducer's cases return the correct type.
21+
export const fooReducer = (state: TFoo, action): TFoo => {
22+
// ...
23+
};
24+
25+
export interface IBar {
26+
a: number;
27+
b: string;
28+
}
29+
30+
export const barReducer = (state: IBar, action): IBar => {
31+
// ...
32+
};
33+
```
34+
35+
Since most applications are composed of several reducers, you should compose
36+
a global 'AppState' by composing the reducer types:
37+
38+
```typescript
39+
export interface IAppState {
40+
foo?: TFoo;
41+
bar?: IBar;
42+
}
43+
44+
export const rootReducer = combineReducers({
45+
foo: fooReducer,
46+
bar: barReducer
47+
});
48+
```
49+
50+
This 'app state' is what you should use when injecting `NgRedux`:
51+
52+
```typescript
53+
import { Injectable } from 'ng2-redux';
54+
import { IAppState } from './store';
55+
56+
@Injectable()
57+
export class MyActionService {
58+
constructor(private ngRedux: NgRedux<IAppState>) {}
59+
60+
// ...
61+
}
62+
```
63+
64+
### Consider Using Built-In Types from Redux
65+
66+
Redux ships with a good set of official typings; consider using them. In
67+
particular, consider importing and using the `Action` and `Reducer` types:
68+
69+
```typescript
70+
import { Action, Reducer } from 'redux';
71+
72+
export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action): TFoo => {
73+
// ...
74+
};
75+
```
76+
77+
Note that we supply this reducer's state type as a generic type parameter to `Reducer<T>`.
78+
79+
### Consider using 'Flux Standard Actions' (FSAs)
80+
81+
[FSA](https://github.com/acdlite/flux-standard-action/blob/master/src/index.js)
82+
is a widely-used convention for defining the shape of actions. You can import
83+
in into your project and use it:
84+
85+
```sh
86+
npm install --save flux-standard-action @types/flux-standard-action
87+
```
88+
89+
Flux standard actions take 'payload', and 'error' parameters in addition to the
90+
basic `type`. Payloads in particular help you strengthen your reducers even
91+
further:
92+
93+
```typescript
94+
import { Reducer } from 'redux';
95+
import { Action } from 'flux-standard-action';
96+
97+
export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action<TFoo>): TFoo => {
98+
// ...
99+
};
100+
```
101+
102+
Here we're saying that the action's payload must have type TFoo.
103+
If you need more flexibility in payload types, you can use a union and
104+
[type assertions](https://www.typescriptlang.org/docs/handbook/advanced-types.html):
105+
106+
```typescript
107+
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number | string>): IBar => {
108+
switch(action.type) {
109+
case A_HAS_CHANGED:
110+
return Object.assign({}, state, {
111+
a: <number>action.payload
112+
});
113+
case B_HAS_CHANGED:
114+
return Object.assign({}, state, {
115+
b: <string>action.payload
116+
});
117+
// ...
118+
}
119+
};
120+
```
121+
122+
For more complex union-payload scenarios, Typescript's [type-guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html) may also be helpful.
123+
124+
### Use a Typed Wrapper around Object.assign
125+
126+
In the Babel world, reducers often use `Object.assign` or property spread to
127+
maintain immutability. This works in Typescript too, but it's not typesafe:
128+
129+
```typescript
130+
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number | string>): IBar => {
131+
switch(action.type) {
132+
case A_HAS_CHANGED:
133+
return Object.assign({}, state, {
134+
a: <number>action.payload,
135+
zzz: 'test' // We'd like this to generate a compile error, but it doesn't
136+
});
137+
// ...
138+
}
139+
};
140+
```
141+
142+
Ideally, we'd like this code to fail because `zzz` is not a property of the state.
143+
However, the built-in type definitions for `Object.assign` return an intersection
144+
type, making this legal. This makes sense for general usage of `Object.assign`,
145+
but it's not what we want in a reducer.
146+
147+
Instead, we've provided a type-corrected immutable assignment function, [`tassign`](https://npmjs.com/package/tassign),
148+
that will catch this type of error:
149+
150+
```typescript
151+
import { tassign } from 'tassign';
152+
153+
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number | string>): IBar => {
154+
switch(action.type) {
155+
case A_HAS_CHANGED:
156+
return tassign(state, {
157+
a: <number>action.payload,
158+
zzz: 'test' // Error: zzz is not a property of IBar
159+
});
160+
// ...
161+
}
162+
};
163+
```
164+
165+
Following these tips to strengthen your reducer typings will go a long way
166+
towards more robust code.

examples/counter/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@
3232
"core-js": "^2.3.0",
3333
"ng2-redux": "^4.0.0-beta.7",
3434
"redux": "^3.5.0",
35-
"redux-logger": "^2.6.1",
3635
"redux-localstorage": "^0.4.0",
36+
"redux-logger": "^2.6.1",
3737
"reflect-metadata": "0.1.3",
3838
"rxjs": "5.0.0-beta.12",
39+
"tassign": "^1.0.0",
3940
"zone.js": "^0.6.21"
4041
},
4142
"devDependencies": {

examples/counter/store/search.reducer.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tassign } from 'tassign';
12
import { SEARCH_ACTIONS } from '../actions/search.actions';
23

34
export interface ISearchState {
@@ -18,14 +19,14 @@ export function searchReducer(
1819

1920
switch (action.type) {
2021
case SEARCH_ACTIONS.SEARCH:
21-
return Object.assign({}, state, {
22+
return tassign(state, {
2223
onSearch: true,
2324
keyword: action.payload,
2425
total: state.total
2526
});
2627
case SEARCH_ACTIONS.SEARCH_RESULT:
2728
let total = action.payload.total;
28-
return Object.assign({}, state, {
29+
return tassign(state, {
2930
onSearch: state.onSearch,
3031
keyword: state.keyword,
3132
total

src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { NgRedux } from './components/ng-redux';
22
import { DevToolsExtension } from './components/dev-tools';
33
import { select } from './decorators/select';
44
import { NgReduxModule } from './ng-redux.module';
5-
65

76
export {
87
NgRedux,

src/utils/shallow-equal.spec.ts

+5
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,10 @@ describe('Utils', () => {
5353
)
5454
).to.equal(false);
5555
});
56+
57+
it('should return true for two references to the same thing', () => {
58+
const thing = { a: 1, b: 2, c: undefined };
59+
expect(shallowEqual(thing, thing)).to.equal(true);
60+
});
5661
});
5762
});

0 commit comments

Comments
 (0)