npm package: @katis/typed-redux-actions
Description: Easy typed Redux actions without boilerplate
Author: Joni Katajamäki
Latest version: 10.1.0

[![npm version](https://badge.fury.io/js/%40katis%2Ftyped-redux-actions.svg)](https://badge.fury.io/js/%40katis%2Ftyped-redux-actions)

# 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:

```ts
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

```ts
// 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:

```ts
// 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:

```ts
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":

```ts
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>;
```