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 ad2b723
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 3 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: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@
"require": [
"ts-node/register"
],
"reporter": [
"lcov", "text"
],
"reporter": [ "lcov", "text" ],
"all": true,
"check-coverage": true,
"lines": 80,
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 ad2b723

Please sign in to comment.