Skip to content

Latest commit

 

History

History
187 lines (163 loc) · 8.04 KB

README.md

File metadata and controls

187 lines (163 loc) · 8.04 KB

CircleCI Build Status npm

redux-actions-ts-reducer

Helper for writing type-safe and bloat-free reducers using redux-actions and TypeScript

Helps You to write reducers for redux-actions actions in TypeScript in type-safe manner and without needing to specify excessive type information - everything is inferred based on initial state and types of redux-action action creators.

Table of Contents

Getting Started

Prerequisites

Assuming that you have already installed

  1. redux-actions (since this library uses internally handleActions(previouslyAddedReducersAsMap) from redux-actions and can infer reducer action.payload type based on action creator type that is created with createAction<actionPayloadType>(...))
  2. TypeScript type declarations @types/redux-actions (since using this libary makes most sense with TypeScript that can detect problems at compile-time)

Installation

$ npm install redux-actions-ts-reducer --save

or

$ yarn add redux-actions-ts-reducer

Usage

No boilerblate, type-safe

See comments in the example code bellow:

import { ReducerFactory } from 'redux-actions-ts-reducer';
import { createAction } from 'redux-actions';

// Sample action creators(redux-actions) and action type constants that reducer can handle
const negate = createAction('NEGATE'); // action without payload
const add = createAction<number>('ADD'); // action with payload type `number`
const SOME_LIB_NO_ARGS_ACTION_TYPE = '@@some-lib/NO_ARGS_ACTION_TYPE'; // could be useful when action type like this is defined by 3rd party library
const SOME_LIB_STRING_ACTION_TYPE = '@@some-lib/STRING_ACTION_TYPE'; // could be useful when action type like this is defined by 3rd party library

// type of the state
class SampleState {
	count = 0;
	message: string = null;
}

// creating reducer that combines several reducers
const reducer = new ReducerFactory(new SampleState())
	// `state` argument and return type is inferred based on `new ReducerFactory(initialState)`.
	// Type of `action.payload` is inferred based on first argument (action creator)
	.addReducer(add, (state, action) => {
		return {
			...state,
			count: state.count + action.payload,
		};
	})
	// no point to add `action` argument to reducer in this case, as `action.payload` type would be `void` (and effectively useless)
	.addReducer(negate, (state) => {
		return {
			...state,
			count: state.count * -1,
		};
	})
	// when adding reducer for action using string actionType
	// (using `addReducer(actionType: string, reducerFunction)` instead of `redux-actions` actionCreator, that can be created with `createAction(...)`)
	// You should tell what is the action payload type using generic argument (if You plan to use `action.payload`)
	.addReducer<string>(SOME_LIB_STRING_ACTION_TYPE, (state, action) => {
		return {
			...state,
			message: action.payload,
		};
	})
	// action.payload type is `void` by default when adding reducer function using `addReducer(actionType: string, reducerFunction)`
	.addReducer(SOME_LIB_NO_ARGS_ACTION_TYPE, (state) => {
		return new SampleState();
	})
	// creates reducer (implementation delegates to `handleActions(previouslyAddedReducersAsMap)` in `redux-actions` package)
	.toReducer();

export default reducer;

As You can see, You don't need to specify types of reducer function state and action parameters or reducer function return type - State types for reducer functions (reducer function state argument and return type) are inferred based on initial state (new ReducerFactory(initialState)). Type of action (and action.payload) are inferred based on either

a) first argument type when passing in action creator as first argument (using ReducerFactory.addReducer(actionCreator: ActionFunctions, reducerFunction))

b) (optional) generic type of addReducer that defaults to void (using ReducerFactory.addReducer<ActionPayloadType>(actionType: string, reducerFunction))

So You don't need to specify any TypeScript type annotations for any parameters or return types. This even works with (noImplicitAny TypeScript compiler option) - how cool is that?

But if You want to add reducer function by action type (instead of redux-actions action creator), and You want to use action.payload, then it isn't possible for TypeScript compiler to figure out what the payload type should be, so You must provide its type via generic type parameter of addReducer<ActionPayloadType>(...).

Type-safety to the limits

Note, that while TypeScript compiler prevents You from returning less properties than present on state type, it allows You to return more properties than present on state type if state type is inferred. That would be highly unlikely a problem if You don't use object spread syntax on state object to create new state:

		return {
			message: state.message,
			count: state.count + action.payload,
			thisPropertyDoesNotExist: 'Oops! this problem is detected by TypeScript compiler if return type is explicitly set on the reducer arrow function',
		};

(as you probably wouldn't mistype the property that didn't exist on state type), but it could become a problem if You do use object spread syntax on state object to create new state:

		return {
			...state,
			typoInPropertyName: state.count + action.payload, // unintended assignment to wrong property
			thisPropertyDoesNotExist: 'Oops! this problem is detected by TypeScript compiler if return type is explicitly set on the reducer arrow function',
		};

as You may mistype property name You want to assign, resulting in incorrect code without TypeScript error.

So if You want to be super-safe and catch this kind of errors with TypeScript compiler, You should explicitly add return type for added reducer functions:

     // .addReducer(add, (state, action) => { // wouldn't detect following problem
	.addReducer(add, (state, action): SampleState => { // would detect following problem
		return {
			...state,
			count: state.count + action.payload,
			thisPropertyDoesNotExist: 'Oops! this problem is detected by TypeScript compiler if return type is explicitly set on the reducer arrow function',
			// ^^^ Error: TS2322: Type ... is not assignable to type 'SampleState'.
			// Object literal may only specify known properties, and 'thisPropertyDoesNotExist' does not exist in type 'SampleState'.
		};
	})

It is up to You to decide if You want to add return type explicitly to gain maximum type safety or leave it out (and perhaps catch these potential issues with automated or manual tests) and still get fairly good type safety.

Code style

You don't need to define class for state type, even tough this is my personal perference:

class SampleState {
	count = 0;
	message: string = null;
}
const reducer = new ReducerFactory(new SampleState())

Alternatively You could write:

const initialState = {
	count: 0,
	message: null as string,
};
const sampleReducer = new ReducerFactory(initialState)

But it is a matter of code style and totally up to You.

More information

  • tests cover all use-cases
  • API documentation (generated TypeScript type definitions file based on source code and comments)