We'll be using a battle-tested library
that automates and simplify maintenace of type annotations in Redux Architectures
typesafe-actions
You should read The Mighty Tutorial to learn it all the easy way!
A solution below is using simple factory function to automate the creation of type-safe action creators. The goal is to reduce the maintainability and code repetition of type annotations for actions and creators and the result is completely typesafe action-creators and their actions.
::example='../../playground/src/features/counters/actions.ts':: ::usage='../../playground/src/features/counters/actions.usage.ts'::
Declare reducer State
type with readonly
modifier to get compile time immutability
export type State = {
readonly counter: number;
readonly todos: ReadonlyArray<string>;
};
Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors
export const initialState: State = {
counter: 0,
}; // OK
initialState.counter = 3; // TS Error: cannot be mutated
It's great for Arrays in JS because it will error when using mutator methods like (push
, pop
, splice
, ...), but it'll still allow immutable methods like (concat
, map
, slice
,...).
state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray<string>'
const newTodos = state.todos.concat('Learn about tagged union types') // OK
This means that the readonly
modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly.
To fix this we can use DeepReadonly
type (available in utility-types
npm library - collection of reusable types extending the collection of standard-lib in TypeScript.
Check the example below:
import { DeepReadonly } from 'utility-types';
export type State = DeepReadonly<{
containerObject: {
innerValue: number,
numbers: number[],
}
}>;
state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated
state.containerObject.innerValue = 1; // TS Error: cannot be mutated
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods
use
Readonly
orReadonlyArray
Mapped types
export type State = Readonly<{
counterPairs: ReadonlyArray<Readonly<{
immutableCounter1: number,
immutableCounter2: number,
}>>,
}>;
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
to understand following section make sure to learn about Type Inference, Control flow analysis and Tagged union types
::example='../../playground/src/features/todos/reducer.ts'::
::example='../../playground/src/features/todos/reducer.spec.ts'::
Can be imported in connected components to provide type-safety to Redux connect
function
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics
::example='../../playground/src/store/types.d.ts'::
When creating a store instance we don't need to provide any additional types. It will set-up a type-safe Store instance using type inference.
The resulting store instance methods like
getState
ordispatch
will be type checked and will expose all type errors
::example='../../playground/src/store/store.ts'::
For more examples and in-depth explanation you should read The Mighty Tutorial to learn it all the easy way!
::example='../../playground/src/features/todos/epics.ts'::
::example='../../playground/src/features/todos/selectors.ts'::
Below snippet can be find in the playground/
folder, you can checkout the repo and follow all dependencies to understand the bigger picture.
playground/src/connected/sfc-counter-connected-verbose.tsx
import Types from 'Types';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { countersActions } from '../features/counters';
import { SFCCounter, SFCCounterProps } from '../components';
// `state` parameter needs a type annotation to type-check the correct shape of a state object but also it'll be used by "type inference" to infer the type of returned props
const mapStateToProps = (state: Types.RootState, ownProps: SFCCounterProps) => ({
count: state.counters.reduxCounter,
});
// `dispatch` parameter needs a type annotation to type-check the correct shape of an action object when using dispatch function
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) => bindActionCreators({
onIncrement: countersActions.increment,
// without using action creators, this will be validated using your RootAction union type
// onIncrement: () => dispatch({ type: "counters/INCREMENT" }),
}, dispatch);
// NOTE: We don't need to pass generic type arguments to neither connect nor mapping functions because type inference will do all this work automatically. So there's really no reason to increase the noise ratio in your codebase!
export const SFCCounterConnectedVerbose =
connect(mapStateToProps, mapDispatchToProps)(SFCCounter);