From 42744abec5a54ea1df0e3fb932ab43731355c878 Mon Sep 17 00:00:00 2001 From: Seth Davenport Date: Sun, 4 Sep 2016 22:59:16 -0400 Subject: [PATCH] Added tassign, docs about typing reducers Fixes #216 --- README.md | 1 + docs/strongly-typed-reducers.md | 133 ++++++++++++++++++++++++++++++++ examples/counter/package.json | 4 +- package.json | 4 +- src/index.ts | 1 + src/utils/tassign.ts | 3 + 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 docs/strongly-typed-reducers.md create mode 100644 src/utils/tassign.ts diff --git a/README.md b/README.md index 4cee6b0..5c23aa4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/strongly-typed-reducers.md b/docs/strongly-typed-reducers.md new file mode 100644 index 0000000..19d84e5 --- /dev/null +++ b/docs/strongly-typed-reducers.md @@ -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 = (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 etc. +export const fooReducer: Reducer = (state: TFoo, action: Action): 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 = (state: IBar, action: Action): 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 = (state: IBar, action: Action): 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. diff --git a/examples/counter/package.json b/examples/counter/package.json index a802bc0..5405e86 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -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", diff --git a/package.json b/package.json index cbb8cdc..90e7862 100644 --- a/package.json +++ b/package.json @@ -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", @@ -95,7 +96,8 @@ "ts-node/register" ], "reporter": [ - "lcov", "text" + "lcov", + "text" ], "all": true, "check-coverage": true, diff --git a/src/index.ts b/src/index.ts index 74c8e6f..33533ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/utils/tassign.ts b/src/utils/tassign.ts new file mode 100644 index 0000000..439ecf2 --- /dev/null +++ b/src/utils/tassign.ts @@ -0,0 +1,3 @@ +export function tassign(target: T, ...source: U[]): T { + return Object.assign({}, target, ...source); +} \ No newline at end of file