typed-actions
v0.12.0
Published
Typed actions for redux
Downloads
32
Readme
typed-actions
Some Types and Utils (based on redux-actions way) to create type-safe actions, reducers, and epics with auto-inferred types.
Main points:
- 100% Flow coverage for the redux-side with minimum typings (auto-inferred types) and less boilerplate
- Safe types and functions, which help to reduce risks for Type mistakes
- Accurate Type-errors handling
- Deep immutable Type (
Frozen
) for actions and redux state - Immer support
Installation
npm install typed-actions
Usage
You can get some examples here with explanations.
Actions
Actions are compatible with Flux Standard Action
action(payload, ?meta)
produces{type, payload} | {type, payload, meta}
error(payload, ?meta)
produces{type, payload, error: true} | {type, payload, meta, error: true}
empty()
produces{type}
import {createActions, action, empty} from 'typed-actions'
import type {EntityId} from '../types';
/**
* Declare Action Types as constants and export them
*/
export const UPDATE = '@namespace/UPDATE'
export const UPDATE_FULFILLED = '@namespace/UPDATE_FULFILLED'
export const UPDATE_FAILED = '@namespace/UPDATE_FAILED'
/**
* Create Actions Collection
*/
const actions = createActions({
/**
* {type: UPDATE}
*/
[UPDATE]: empty,
/**
* {type: UPDATE_FULFILLED, payload: EntityId[]}
*/
[UPDATE_FULFILLED]: (x: EntityId[]) => action(x),
/**
* {type: UPDATE_FAILED, payload: EntityId, meta: {sync: true}}
*/
[UPDATE_FAILED]: (x: EntityId) => action(x, {sync: true}),
})
/**
* Export Action Creators
*/
export const {
[UPDATE]: update,
[UPDATE_FULFILLED]: updateFulfilled,
[UPDATE_FAILED]: updateFailed,
} = actions
/**
* Export Collection Type
*/
export type Actions = typeof actions
You might find this declaration style more readable:
let actions
export const {
[UPDATE]: update,
[UPDATE_FULFILLED]: updateFulfilled,
[UPDATE_FAILED]: updateFailed,
} = actions = createActions({
[UPDATE]: empty,
[UPDATE_FULFILLED]: (x: EntityId[]) => action(x)
[UPDATE_FAILED]: (x: EntityId) => action(x, {sync: true}),
})
export type Actions = typeof actions
Reducers
import {handleActions, type Handlers, type Frozen} from 'typed-actions';
import type {EntityId} from '../types';
/**
* Import action types with Actions Collection Type
*/
import {
type Actions,
UPDATE,
UPDATE_FULFILLED,
UPDATE_FAILED,
} from './actions';
/**
* Declare State for the reducer
*/
export type State = {
data: EntityId[],
status: 'done' | 'pending' | 'failed',
};
/**
* Use Handlers Type for the type-casting.
* This way functions will get right arguments Types
*/
export default handleActions(({
/**
* No need to point the State Type in arguments,
* it would be auto-inferred Deep Immutable Type of State
*/
[UPDATE]: state => ({
...state,
status: 'pending',
}),
/**
* The second argument (action) will also have the right type
* {type: UPDATE_FULFILLED, payload: EntityId[]}
*/
[UPDATE_FULFILLED]: (state, {payload}) => ({
...state,
data: payload,
status: 'done',
}),
[UPDATE_FAILED]: state => ({
...state,
status: 'failed',
}),
}: Handlers<State, Actions>));
Immer
You can use immer for immutable state modifying.
Note that
handler
accepts 3 arguments:draft state
(for changes),action
andcurrent state
.
import { type Handlers, handleActions } from 'typed-actions/immer'
import type {EntityId} from '../types';
import {
type Actions,
UPDATE,
UPDATE_FULFILLED,
UPDATE_FAILED,
} from './actions';
export type State = {
data: EntityId[],
status: 'done' | 'pending' | 'failed',
};
export default handleActions(({
[UPDATE]: state => {
state.status = 'pending'
},
[UPDATE_FULFILLED]: (state, {payload}) => {
state.data = payload
state.status = 'done'
},
[UPDATE_FAILED]: state => {
state.status = 'failed'
},
}: Handlers<State, Actions>));
Epics
If you're using redux-observable, this Epic Type could be useful.
import type {Epic} from 'typed-actions/redux-observable';
import {
type Actions,
UPDATE_FULFILLED,
anotherOneAction,
} from './actions';
import {type State} from './reducers';
export const updateEpic: Epic<State, Actions> = action$ => action$
.ofType(UPDATE_FULFILLED)
/**
* No need to add types here, action would be the right Type out of the box
*/
.map(action => anotherOneAction(action.payload));
export const unionEpic: Epic<State, Actions> = action$ => action$
.ofType(UPDATE, UPDATE_FULFILLED)
/**
* {type: UPDATE} | {type: UPDATE_FULFILLED, payload: EntityId[]}
*/
.map(action => {
if (action.type === UPDATE_FULFILLED) {
/**
* Type Refinement will work as well
*
* {type: UPDATE_FULFILLED, payload: EntityId[]}
*/
console.log(action.payload)
}
});
Just to note, if you're using dependencies
in epics, you can pass its type in as the third argument.
type Dependencies = {api: () => void}
export const epic: Epic<State, Actions, Dependencies> = action$ => action$
The recommended way is to redeclare your own Epic
type with custom dependencies and use it, like:
// types.js
import type {Epic} from 'typed-actions/redux-observable'
type Dependencies = {api: () => void}
export type Epic<S, A, D = Dependencies> = Epic<S, A, D>
// epics.js
import type {Epic} from './types'
export const epic: Epic<State, Actions> = action$ => action$