Typed Redux actions
Install
yarn add @katis/typed-redux-actions
Description
FSA compliant type safe Redux action creator utilities.
Using vanilla Redux with TypeScript can be tedious, since you have to repeat yourself multiple times:
const PLUS = 'ADD';
const MINUS = 'SUBTRACT';
const DIV = 'DIVIDE';
const CLEAR = 'CLEAR';
const INVALID = 'INVALID';
interface PlusAction { type: 'ADD'; payload: { add: number }; }
interface MinusAction { type: 'SUBTRACT'; payload: { subtract: number }; }
type DivAction = { type: 'DIVIDE', payload: { divide: number } } | { type: 'DIVIDE', payload: Error, error: true };
interface ClearAction { type: 'CLEAR'; }
interface InvalidAction { type: 'INVALID'; error: true; payload: Error; }
type Action = PlusAction | MinusAction | DivAction | ClearAction | InvalidAction;
const Action = {
plus: (payload: { add: number }): PlusAction => ({ type: PLUS, payload }),
minus: (payload: { subtract: number }): MinusAction => ({ type: MINUS, payload }),
div: (payload: { divide: number } | Error): DivAction => payload instanceof Error ? { type: DIV, payload, error: true } : { type: DIV, payload },
clear: (): ClearAction => ({ type: CLEAR }),
invalid: (error: Error): InvalidAction => ({ type: INVALID, error: true, payload: error }),
};
With typed-redux-actions
, you can automatically derive action type constants and actions union type from your action creators:
Action creator builders
// actions.ts
import { action } from '@katis/typed-redux-actions';
export const plus = action('plus')
.payload<{ add: number }>()
export const minus = action('minus')
.payload<{ subtract: number }>()
export const div = action('div')
.payload<{ divide: number }>()
.canFail()
export const clear = action('clear')
export const invalid = action('invalid')
.error()
Usage in a reducer:
// reducer.ts
import { ActionsUnion, isError } from '@katis/typed-redux-actions';
import * as Action from './actions.ts'
interface State {
result: number;
error: string;
}
type Action = ActionsUnion<typeof Action>;
function reducer(state: State = initialState, action: Action) {
switch (action.type) {
case Action.plus.type:
return { ...state, result: state.result + action.payload.add };
case Action.minus.type:
return { ...state, result: state.result - action.payload.subtract };
case Action.div.type:
if (isError(action)) {
return { ...state, error: 'Tried to divide by zero.' };
}
return { ...state, result: state.result / action.payload.divide };
case Action.clear.type:
return { ...state, result: 0 };
case Action.invalid.type:
return { ...state, error: 'Invalid operation' };
default:
return state
}
}
makeReducer
makeReducer
uses type inference effectively to remove explicit type annotations and switch cases in reducer
definition, while still keeping everything statically typed:
import { makeReducer } from '@katis/typed-redux-actions'
import * as Action from './actions.ts'
const initialState = {
result: 0,
error: '',
};
const reducer = makeReducer(initialState, Action)({
plus: (state, { payload }) => ({ ...state, result: 0 + 2 }),
minus: (state, { payload }) => ({ ...state, result: state.result - payload.subtract }),
div: (state, action) => isError(action) ?
{ ...state, error: 'Tried to divide by zero' } :
{ ...state, result: state.result / action.payload.divide },
clear: state => ({ ...state, result: 0 }),
invalid: state => ({ ...state, error: 'Invalid operation' }),
});
ReducerState
You can easily get the full combined Redux state with this type level "function":
import { combineReducers } from 'redux'
import { ReducerState } from '@katis/typed-redux-actions'
import { localeReducer } from './localeReducer';
import { calculatorReducer } from './calculatorReducer';
const reducer = combineReducers({
locale: localeReducer,
calculator: calculatorReducer,
});
type State = ReducerState<typeof reducer>;