Skip to content

Commit ad2b723

Browse files
author
Seth Davenport
committed
Added tassign, docs about typing reducers
Fixes angular-redux#216
1 parent a059179 commit ad2b723

File tree

5 files changed

+139
-3
lines changed

5 files changed

+139
-3
lines changed

README.md

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

docs/strongly-typed-reducers.md

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 build 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 type 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+
### Consider Using types from `redux`
51+
52+
Redux ships with a good set of official typings; consider using them. In
53+
particular, be aware of the 'Action' and 'Reducer' types:
54+
55+
```typescript
56+
import { Action, Reducer } from 'redux';
57+
58+
export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action): TFoo => {
59+
// ...
60+
};
61+
```
62+
63+
Note that we supply the state type as a generic type parameter to `Reducer`.
64+
65+
### Consider using 'Flux Standard Actions' (FSAs)
66+
67+
[FSA](https://github.com/acdlite/flux-standard-action/blob/master/src/index.js)
68+
is a widely-used convention for defining the shape of actions. You can import
69+
in into your project and use it:
70+
71+
```sh
72+
npm install --save flux-standard-action @types/flux-standard-action
73+
```
74+
75+
Flux standard actions take 'payload', and 'error' parameters in addition to the
76+
basic `type`. Payloads in particular help you strengthen your reducers even
77+
further:
78+
79+
```typescript
80+
import { Reducer } from 'redux';
81+
import { Action } from 'flux-standard-action';
82+
83+
// Here we're saying that the action's payload must have type TFoo.
84+
// If you need more flexibility in payload types, you can use a union and
85+
// typeguards: Action<TFoo : IBar> etc.
86+
export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action<TFoo>): TFoo => {
87+
// ...
88+
};
89+
```
90+
91+
### Use a Typed Wrapper around Object.assign
92+
93+
In the Babel world, reducers often use `Object.assign` or property spread to
94+
maintain immutability. This works in Typescript too, but it's not typesafe:
95+
96+
```typescript
97+
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number>): IBar => {
98+
switch(action.type) {
99+
case A_HAS_CHANGED:
100+
return Object.assign({}, state, {
101+
a: action.payload,
102+
zzz: 'test'
103+
});
104+
// ...
105+
}
106+
};
107+
```
108+
109+
Ideally, we'd like this code to fail because `zzz` is not a property of the state.
110+
However, the built-in type definitions for `Object.assign` return an intersection
111+
type, making this legal. This makes sense for general usage of `Object.assign`,
112+
but it's not what we want in a reducer.
113+
114+
Instead, we've provided a type-corrected immutable assignment function, `tassign`,
115+
that will catch this type of error:
116+
117+
```typescript
118+
import { tassign } from 'ng2-redux';
119+
120+
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number>): IBar => {
121+
switch(action.type) {
122+
case A_HAS_CHANGED:
123+
return tassign(state, {
124+
a: action.payload,
125+
zzz: 'test' // Error: zzz is not a property of IBar
126+
});
127+
// ...
128+
}
129+
};
130+
```
131+
132+
Following these tips to strengthen your reducer typings will go a long way
133+
towards more robust code.

package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,7 @@
9494
"require": [
9595
"ts-node/register"
9696
],
97-
"reporter": [
98-
"lcov", "text"
99-
],
97+
"reporter": [ "lcov", "text" ],
10098
"all": true,
10199
"check-coverage": true,
102100
"lines": 80,

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { NgRedux } from './components/ng-redux';
22
export { DevToolsExtension } from './components/dev-tools';
33
export { select } from './decorators/select';
4+
export { tassign } from './utils/tassign';

src/utils/tassign.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function tassign<T extends U, U>(target: T, ...source: U[]): T {
2+
return Object.assign({}, target, ...source);
3+
}

0 commit comments

Comments
 (0)