0.1.0 • Published 6 years ago

strong-action v0.1.0

Weekly downloads
2
License
MIT
Repository
github
Last release
6 years ago

strong-action

TypeScript definitions for strongly-typed Redux actions

Usage

Define your actions as string literals:

export const ADD_ITEM = 'ADD_ITEM';
export const DELETE_ITEM = 'DELETE_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';

Create an ActionCreatorsMapObject (implicitly) by creating an object with keys representing each action and values with action creators that use the included defineAction rather than returning the actions directly:

export const Actions = {
  addItem: (payload: ItemData) => defineAction(ADD_ITEM, payload),
  deleteItem: (payload: number) => defineAction(DELETE_ITEM, payload),
  updateItem: (payload: Partial<ItemData>) => defineAction(UPDATE_ITEM, payload)
}

Then create an AllActions type with the same name as your ActionCreatorsMapObject. This type will represent the the union of all of the action types:

export type Actions = AllActions<typeof Actions>;

The value of this setup becomes clear in the reducer. By switching on action.type, the TypeScript compiler can infer which action types are valid, and within each action case it will infer the correct payload type:

export const reducer = (state = initialState, action: Actions): State => {
  switch(action.type) {
    case ADD_ITEM: {
      // compiler knows that action.payload is ItemData
      return state;
    }
    
    case DELETE_ITEM: {
      // compiler knows that action.payload is number
      return state;
    }
    
    case UPDATE_ITEM: {
      // compiler knows that action.payload is Partial<ItemData>
      return state;
    }
    // the compiler will not let you specify invalid actions, and as long as the
    // reducer's return type is specified and strictNullChecks is enabled, the
    // compiler will also ensure that don't omit any valid actions
  }
}

Rationale

The basic idea is that we want the TypeScript compiler to help us ensure our actions are defined and used consistently, eliminating an entire class of errors. This has been a challenge in the past (without resorting to a class-based solution that doesn't work well with plain Redux), but TypeScript 2.8+ provides features that make it easy.

The naive approach to using TypeScript with Redux actions is to manually create types representing the return types of your action creators, then to actually implement the action creators. This leads to a lot of unwanted repitition and the need to change code in more than one place to change functionality.

An improvement is to generate the action creator return types using the ReturnType<T> mapped type on the action creator (the return type of the action creator being the action type). However, since doing so flattens the action's type property to a plain string (rather than a string literal), the type of the action type has to be explicitly cast to the string literal type in the action creator's returned object. This works but is verbose and a vector for error due to the explicit cast.

The approach used here (implementation described above) produces action creators that preserve their string literal type property and generates a union type of all valid actions using type declaration merging.

Once this is done, the actions act as discriminated unions, allowing the compiler to know which actions are valid, invalid, or missing and what payload types are associated with each action in the reducer (or anywhere).