Type-safe Redux utilities for TypeScript


Keywords
tsdux, redux, typescript, type-safe, state
License
MIT
Install
npm install tsdux@4.0.1

Documentation

tsdux

npm latest version npm total download github license github latest tag github commit from latest travis status codecov coverage

Type-safe Redux Utils for TypeScript!

Table of Contents

How To Install

npm install --save redux tsdux

If you use redux-observable or use RxJS with redux, you also might have interest in tsdux-observable which includes some basic utilities to use TSdux with Observables.

npm install --save rxjs tsdux-observable

How To Use

// mysection.ts
import { action, payload, union } from 'tsdux';

interface MySectionState {
  log?: string;
  id: number;
}

// Make action creators
export const MyAction = action('myapp/mysection/MY_ACTION', props<{ error?: string }>());
export const YourAction = action('myapp/mysection/YOUR_ACTION', payload<{ id: number }>());

const Action = union([
  MyAction,
  YourAction,
]);
type Action = typeof Action;

const initialState: MySectionState = {
  id: 0,
};
export default function reducer(state: MySectionState = initialState, action: ActionType): MySectionState {
  switch(action.type) {
  case MyAction.type:
    return {
      ...state,
      log: action.error,
    };
  case YourAction.type:
    return {
      ...state,
      id: action.payload.id,
    };
  default:
    return state;
  }
}
// At other part that uses redux
import { MyAction, YourAction } from './mysection';
import { store } from './store';

store.dispatch(MyAction('abcd'));
store.dispatch(YourAction({ id: 5 }));

Prior Arts

This library is highly inspired by ts-action. However, it uses Object.setPrototypeOf in its code, and it is really, really bad to performace (see MDN) and have some runtime overheads on the ActionCtor instances. This library is intended to be more fast, and be more lighter (therefore, have less APIs...) than ts-action (At least this does not use Object.setPrototypeOf!). If you find any codes those hurt performance, please report an issue.

API

API Types

Types used in this library.

TypeOnlyAction

Action without any extra properties.

type TypeOnlyAction<T extends string> = {
  type: T;
}

PropsAction

Action with extra properties.

type PropsAction<T extends string, P extends object = {}> = {
  type: T;
} & P;

PayloadAction

Action with payload property. This action is easier to create than PropsAction, although this also has its own cons.

type PayloadAction<T extends string, P = {}> = {
  type: T;
  payload: P;
}

TypeOnlyActionCreator

ActionCreator made by action function. This ActionCreator creates TypeOnlyAction.

interface TypeOnlyActionCreator<T extends string> {
  (): TypeOnlyAction<T>;
  type: T;
  action: TypeOnlyAction<T>;
  create(): TypeOnlyAction<T>;
}

PropsActionCreator

This ActionCreator creates PropsAction.

interface PropsActionCreator<T extends string, P extends object = {}> {
  (props: P): PropsAction<T, P>;
  type: T;
  action: PropsAction<T, P>;
  create(props: P): PropsAction<T, P>;
}

PayloadActionCreator

This ActionCreator creates PayloadAction.

interface PayloadActionCreator<T extends string, P = {}> {
  (payload: P): PayloadAction<T, P>;
  type: T;
  action: PayloadAction<T, P>;
  create(payload: P): PayloadAction<T, P>;
}

Action util functions

Functions for defining an action (or rather an action creator). Defined action creators are used to define reducer.

action

function action<T extends string>(type: T): TypeOnlyActionCreator<T>;
function action<T extends string, P extends object = {}>(type: T, p: PropsOpt<P>): PropsActionCreator<T, P>;
function action<T extends string, P = {}>(type: T, p: PayloadOpt<P>): PayloadActionCreator<T, P>;

action is for creating an ActionCreator. ActionCreator is object used for this library. You can create action by calling ActionCreator itself or calling actionCreator.create method.

const FixNote = action('FixNote');
const fixNote0 = FixNote.create();
const fixNote1 = FixNote();
console.log(fixNote0); // { type: 'FixNote' }
console.log(fixNote1); // { type: 'FixNote' }
switch (action.type) {
case FixNote.type:
  // ... fix note
default:
  return state;
}

However, if you cannot define payload, this action is useless. You can define it using payload function.

payload

function payload<P = {}>(): PayloadOpt<P>

payload is for creating a payload argument (second argument) of action function.

const AddNote = action('AddNote', payload<string>());
const addNote = AddNote('This is a new note');
console.log(addNote); // { type: 'AddNote', payload: 'This is a new note' }

props

function props<P extends object = {}>(): PropsOpt<Omit<P, 'type'>>

props is for creating a props argument (second argument) of action function.
The difference between props and payload is that props injects additional data to action as property, where payload set additional data to payload of action. To get more clear understanding, see following example.

const ActionFromProps = action('Example0', props<{ x: number }>());
const ActionFromPayload = action('Example1', payload<{ x: number }>());

console.log(ActionFromProps({ x: 5 })); // { type: 'Example0', x: 5 }
console.log(ActionFromPayload({ x: 5 })); // { type: 'Example1', payload: { x: 5 } }

const OnlyForPayload = action('Example2', payload<string>());
// Following codes emit an error.
const ThisDoesNotWork = action('Example3', props<string>()); // There's no way to inject 'string' into action.

console.log(OnlyForPayload('abc')); // { type: 'Example2', payload: 'abc' }
const AddError = action('AddError', props<{ error?: string }>());
const addError0 = AddError({});
const addError1 = AddError({ error: 'New error' });
console.log(addError0); // { type: 'AddError' }
console.log(addError1); // { type: 'AddError', error: 'New error' }

Action type util functions

Functions to define union type (or to do other type-related things).

union

function union<AC extends ActionCreator<string, any>>(arg: Array<AC>): AC['action'];

To define the type including all actions, you need to use this function. This union type is useful when you define a reducer without subreducer and reducer functions.

const NoteActions = union([FixNote, AddNote]);

function reducer(state: State = { notes: [] }, action: NoteActions) {
  switch(action.type) {
  case FixNote.type:
    // ...
  case AddNote.type:
    // ...
  }
}

isType

function isType<AC extends ActionCreator<string, any>>(
  action: AnyAction, actionCreators: AC | Array<AC>,
): action is AC['action']

To check an action is specific type or not, you can use this function.

function reducer(state: State = { notes: [] }, action: NoteActions) {
  if (isType(action, FixNote)) {
    // ...
  } eles if (isType(action, AddNote)) {
    // ...
  } // ...
}

Reducer util functions

Functions for define redux reducer.

subreducer

function subreducer<S, T extends string, P extends object>(
  action: ActionCreator<T, P>, handler: Subreducer<S, T, P>['handler'],
): Subreducer<S, T, P>

Function to define a reducer for specific action.

const fixNoteReducer = subreducer(FixNote, function (state) {
  return {
    ...state,
  });
});

const addNoteReducer = subreducer(AddNote, function (state, payload) {
  return {
    ...state,
    notes: [...state.notes, payload],
  });
});

These Subreducers can be merged using reducer function.

reducer

function reducer<S, SR extends Subreducer<S, string, any>>(
  initialState: S, subreducers: Array<SR>,
): Reducer<S>

Function to define a reducer by merging Subreducer cases.

export reducer({ notes: [] }, [fixNoteReducer, addNoteReducer]);

Code above is similar with

export function reducer(state = { notes: [] }, action) {
  switch (action.type) {
  case FixNote.type:
    //...
  case AddNote.type:
    //...
  // Code above includes following default case too.
  default:
    return state;
  }
}

Author

Junyoung Clare Jang @Ailrun