|
| 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. |
0 commit comments