Skip to content

Commit

Permalink
Added tassign, docs about typing reducers
Browse files Browse the repository at this point in the history
  • Loading branch information
Seth Davenport committed Sep 5, 2016
1 parent a059179 commit 42744ab
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,4 @@ We also have a number of 'cookbooks' for specific Angular 2 topics:
* [Managing Side-Effects with redux-observable Epics](docs/epics.md)
* [Using the Redux DevTools Chrome Extension](docs/redux-dev-tools.md)
* [Ng2Redux and ImmutableJS](docs/immutable-js.md)
* [Strongy Typed Reducers](docs/stronly-typed-reducers.md)
133 changes: 133 additions & 0 deletions docs/strongly-typed-reducers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Strongly Typed Reducers

It's good practice in typescript to be as specific about your types as possible.
This helps you catch errors at compile-time instead of build time.

Reducers are no exception to this rule. However it's not always obvious how to
make this happen in practice.

## Reducer Typing Best Practices

### Define an Interface for your State

It's important to strongly type the data in your store, and this is done by
defining types for the `state` arguments to your reducers`:

```typescript
export type TFoo: string;

// Being explicit about the state argument and type ensures that all your
// reducer's cases return the correct type.
export const fooReducer = (state: TFoo, action): TFoo => {
// ...
};

export interface IBar {
a: number;
b: string;
}

export const barReducer = (state: IBar, action): IBar => {
// ...
};
```

Since most applications are composed of several reducers, you should compose
a global 'AppState' by composing the reducer types:

```typescript
export interface IAppState {
foo: TFoo;
bar: IBar;
}

export const rootReducer = combineReducers({
foo: fooReducer,
bar: barReducer
});
```

### Consider Using types from `redux`

Redux ships with a good set of official typings; consider using them. In
particular, be aware of the 'Action' and 'Reducer' types:

```typescript
import { Action, Reducer } from 'redux';

export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action): TFoo => {
// ...
};
```

Note that we supply the state type as a generic type parameter to `Reducer`.

### Consider using 'Flux Standard Actions' (FSAs)

[FSA](https://github.com/acdlite/flux-standard-action/blob/master/src/index.js)
is a widely-used convention for defining the shape of actions. You can import
in into your project and use it:

```sh
npm install --save flux-standard-action @types/flux-standard-action
```

Flux standard actions take 'payload', and 'error' parameters in addition to the
basic `type`. Payloads in particular help you strengthen your reducers even
further:

```typescript
import { Reducer } from 'redux';
import { Action } from 'flux-standard-action';

// Here we're saying that the action's payload must have type TFoo.
// If you need more flexibility in payload types, you can use a union and
// typeguards: Action<TFoo : IBar> etc.
export const fooReducer: Reducer<TFoo> = (state: TFoo, action: Action<TFoo>): TFoo => {
// ...
};
```

### Use a Typed Wrapper around Object.assign

In the Babel world, reducers often use `Object.assign` or property spread to
maintain immutability. This works in Typescript too, but it's not typesafe:

```typescript
export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number>): IBar => {
switch(action.type) {
case A_HAS_CHANGED:
return Object.assign({}, state, {
a: action.payload,
zzz: 'test'
});
// ...
}
};
```

Ideally, we'd like this code to fail because `zzz` is not a property of the state.
However, the built-in type definitions for `Object.assign` return an intersection
type, making this legal. This makes sense for general usage of `Object.assign`,
but it's not what we want in a reducer.

Instead, we've provided a type-corrected immutable assignment function, `tassign`,
that will catch this type of error:

```typescript
import { tassign } from 'ng2-redux';

export const barReducer: Reducer<IBar> = (state: IBar, action: Action<number>): IBar => {
switch(action.type) {
case A_HAS_CHANGED:
return tassign(state, {
a: action.payload,
zzz: 'test' // Error: zzz is not a property of IBar
});
// ...
}
};
```

Following these tips to strengthen your reducer typings will go a long way
towards more robust code.
4 changes: 3 additions & 1 deletion examples/counter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@
"@angular/platform-browser": "2.0.0-rc.5",
"@angular/platform-browser-dynamic": "2.0.0-rc.5",
"core-js": "^2.3.0",
"flux-standard-action": "^0.6.1",
"ng2-redux": "^3.3.3",
"redux": "^3.5.0",
"redux-logger": "^2.6.1",
"redux-localstorage": "^0.4.0",
"redux-logger": "^2.6.1",
"reflect-metadata": "0.1.3",
"rxjs": "5.0.0-beta.6",
"zone.js": "^0.6.15"
},
"devDependencies": {
"@types/es6-shim": "0.0.30",
"@types/flux-standard-action": "^0.5.28",
"@types/node": "^6.0.36",
"awesome-typescript-loader": "^2.2.1",
"cross-env": "^1.0.7",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@angular/core": "2.0.0-rc.5",
"@types/chai": "^3.4.31",
"@types/es6-shim": "0.0.30",
"@types/flux-standard-action": "^0.5.28",
"@types/mocha": "^2.2.30",
"@types/node": "^6.0.36",
"@types/sinon": "^1.16.28",
Expand Down Expand Up @@ -95,7 +96,8 @@
"ts-node/register"
],
"reporter": [
"lcov", "text"
"lcov",
"text"
],
"all": true,
"check-coverage": true,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { NgRedux } from './components/ng-redux';
export { DevToolsExtension } from './components/dev-tools';
export { select } from './decorators/select';
export { tassign } from './utils/tassign';
3 changes: 3 additions & 0 deletions src/utils/tassign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function tassign<T extends U, U>(target: T, ...source: U[]): T {
return Object.assign({}, target, ...source);
}

0 comments on commit 42744ab

Please sign in to comment.