The big win when switch to TypeScript over vanilla JS is the ability to accurately define your types in Redux. The glue that binds together actions and reducers can be exhaustively checked, which ultimately implies that you and your loving co-workers can ensure that all actions are accounted for. Switch statements no longer require defaults, and more importantly, this may also be applied to situations where the combineReducer function is used. This requires little more than some of the most basic offerings from TypeScript.
Let's get started.
1. Configuration: noImplicitReturns must be set to true.
2. Let’s define our action keys and our action creators:
enum CounterActionKeys {
INCREMENT = "INCREMENT",
DECREMENT = "DECREMENT",
}interface IncrementCounter {
type: CounterActionKeys.INCREMENT,
}interface DecrementCounter {
type: CounterActionKeys.DECREMENT,
}type CounterActions = IncrementCounter | DecrementCounter;
The important part of this code section is the CounterActions which provides the correct shape for our reducer function. This is what will let our independent reducers functions know that it’s missing an action.
3. Next up we create our reducer function:
function counter(counter: number, action: CounterActions) {
switch (action.type) {
case CounterActionKeys.INCREMENT:
return ++counter;
case CounterActionKeys.DECREMENT:
return --counter;
}
}This is not a perfect implementation, but it illustrates the correct typing structure to allow for exhaustive checking in our independent reducer functions. This works with combineReducers. Adding new action creators will force compiler errors in your reducer. This provides a convenient, repeatable, safe way to extend functionality within your redux based application.
Let’s add one more action. Say we want to add a reset button for our counter:
enum CounterActionKeys {
// ...
RESET = "RESET",
}// ...interface ResetCounter {
type: CounterActionKeys.RESET
}type CounterActions =
| IncrementCounter
| DecrementCounter
| ResetCounter;
No surprises here. Our compiler is complaining about our reducer:
Not all code paths return a value.
Bingo! This is the moment of glory right here.
Here’s the final artifact, with an additional reducer function:
// state...interface State {
counter: number,
text: string
}const state: State = {counter: 0, text: ''};// action types...enum CounterActionKeys {
INCREMENT = "INCREMENT",
DECREMENT = "DECREMENT",
RESET = "RESET",
}interface IncrementCounter {
type: CounterActionKeys.INCREMENT,
}interface DecrementCounter {
type: CounterActionKeys.DECREMENT,
}interface ResetCounter {
type: CounterActionKeys.RESET,
}type CounterActions = IncrementCounter | DecrementCounter | ResetCounter;enum TextActionKeys {
ADD_TEXT = "BRIGHTEN_TEXT",
REMOVE_TEXT = "DARKEN_TEXT",
}interface AddText {
type: TextActionKeys.ADD_TEXT,
text: string,
}interface RemoveText {
type: TextActionKeys.REMOVE_TEXT,
}export type TextActionTypes = AddText | RemoveText;// reducers...function counter(counter: number, action: CounterActions) {
switch (action.type) {
case CounterActionKeys.INCREMENT:
return ++counter;
case CounterActionKeys.DECREMENT:
return ++counter;
case CounterActionKeys.RESET:
return 0;
}
}function text(text: string, action: TextActionTypes) {
switch (action.type) {
case TextActionKeys.ADD_TEXT:
return action.text;
case TextActionKeys.REMOVE_TEXT:
return '';
}
};
I hope this is helpful for anyone looking to leverage the power of their compiler alongside redux. These sorts of things allow us to create and refactor faster and with more confidence while eliminating a good deal of the glue-type-testing.
If there’s something I missed, let me know!