Reducing Typescript and Redux Boilerplate with Partial Actions

Static types can help reduce defects in evolving codebases, but their benefits need to balance out against the inevitable boilerplate. We’ve previously used TypeScript to secure the Redux dispatch flow, but the repetition involved didn’t feel good at the time and hasn’t gotten any better with age.

The problem is that we want to use type-narrowing to reveal logical issues, while still being able to describe like actions in generic terms. Consider a family of actions representing the states of an API request:

// actions.ts
type ListRequest = {
  count?: number
  page?: number
}

type Books = {
  resource: string,
  items: Array<Book>
}

export type Action =
  { type: 'LIST_BOOKS_REQUEST', request: ListRequest }
| { type: 'LIST_BOOKS_RESPONSE', request: ListRequest, response: Books }
| { type: 'LIST_BOOKS_ERROR', request: ListRequest, error: string }

We should be able to count on strong, static guarantees anywhere we dispatch or consume the LIST_BOOKS actions.

// reducers/books.ts
import { Action } from '../actions'

type Books = {
  isLoading: boolean,
  error: null | string,
  books: Array<Book>,
}

const initialState = {
  books: [],
  isLoading: false,
  error: null,
}

function books (state: Books = initialState, action: Action): Books {
  switch (action.type) {
    case 'LIST_BOOKS_REQUEST':
      return { ...state, isLoading: true }
    case 'LIST_BOOKS_ERROR':
      return { ...state, isLoading: false, error: action.error }
    case 'LIST_BOOKS_RESPONSE':
      return {
        error: null,
        isLoading: false,
        books: state.books.concat(action.response.items)
      }
    default:
      return state
  }
}

We would see a compiler error if books tried to access an unknown field. Good. But in order to create actions from the LIST_BOOKS family, we need to explicitly set both their types and their contents. In the previous attempt, we needed a fairly hefty definition to be able to represent asynchronous actions in a reasonably generic way:

const _listBooks: ApiAction<ListRequest, Books> = {
  request: (request) =>
    ({ type: 'LIST_BOOKS_REQUEST', request }),
  success: (response, request) =>
    ({ type: 'LIST_BOOKS_RESPONSE', request, response }),
  error: (error, request) =>
    ({ type: 'LIST_BOOKS_ERROR', request, error }),
}

function createApiAction<Q, S>(a: ApiAction<Q, S>, fetch: (q: Q) => Promise<S>) {
  return (req?: Q) => {
    // Make API request and handle result, dispatching
    // appropriate actions along the way
  }
}

export const listBooks = createApiAction(_listBooks, opts => api.books.list(opts))

The ApiAction type enables a generic interface, but the type conversion underpinning it in _listBooks is a compromise. Since we can’t define type strings dynamically, every new API action family we define will need its own implementation. The common interface is better than working with bespoke actions. But it’s hardly a joy.

Partial Actions

The React ecosystem has trained us (quite rightly) to prize obviousness. Using discrete actions to represent changes within the application makes it clear–sometimes painfully clear–to follow what’s going on. We’re about to break all that.

The little bargain we’re about to strike will but save us a heap of boilerplate, but at the cost of discreet actions. We’ll lose a degree of obviousness in reducer logic and middleware. We’ll keep type-safety. Goethe can opine as he will.

Ready?

Returning to our first example, let’s replace the three actions in the LIST_BOOKS family with a single type full of optional properties:

export type Action = {
  type: 'LIST_BOOKS',
  request?: ListRequest,
  response?: Books,
  error?: string,
}

We can think of this as a “partial” action. Like other forms of deferred state, the LIST_BOOKS action will be filled out with additional content as the request proceeds.

Remember that uncomfortable thing we were about to do? This is it. But using this pattern, we can now extract a generic type for the possible values of an API (or really any asynchronous) request:

type ApiAction<Q, S> = {
  request?: Q,
  response?: S,
  error?: string,
}

export type Action =
  ({ type: 'LIST_BOOKS' } & APIAction<ListRequest, Books>)

References to LIST_BOOKS elsewhere will still be narrowed to the correct type, but we can no longer refer to _ERROR and _SUCCESS actions representing the possible outcomes. Instead, we’ll need to derive state from the action’s content.

function books (prevState: Books = initialState, action: Action): Books {
  if (action.type === 'LIST_BOOKS') {
    const state = Object.assign({}, initialState)
    if (action.response) {
      state.books = state.books.concat(action.response.items)
    } else if (action.error) {
      state.error = action.error
    } else {
      state.isLoading = true
    }
    return state
  }
  return prevState
}

Conclusion

The upside? Reusing the type string across multiple states can save quite a bit of ceremony–in this example replacing the actions in a traditional TypeScript/redux implementation, boilerplate dropped by over 50%.

But whether it’s worth breaking the boundary between actions’ responsibilities will depend on the project and team. Partial actions are just a compromise between language and convention that may make sense in one context but not in another.

And as ever, let me know how it goes!




Books Worth Reading

Dry, stagnant, viscous words. When was the last time you walked away from Netflix long enough to read? To really read?

But where print falls short on candy-coated convenience, it fights its way back with ideas. Reading leaves time to pause, reread, and reflect on what’s just been said–a significant (if underrated) advantage over broadcast media. Want to think? First, read.

Reviews are one way to channel the effort. Knowing there’s a test (even a self-imposed one) at the end of the book encourages focus. Writing a review invites reflection on pivotal scenes and the story’s themes. And it yields a ready-packed answer to “should I read it?” and, “why?”

Treating reviews as recommendations makes it easier to separate out the “skims” from the “studies,” and to walk away from mediocre stories mid-flight. Save time for the books worth reading: packed with ideas, dripping with detail; rich stories filled with vivid imagery. Reviews help clarify the distinction.

If a story’s worth finishing, it’s worth taking a moment to mull over–and share–its big ideas. Down goes the book. Out comes the pen. And let me know how it goes!



Book Review: The Essential Drucker

Peter Drucker has a certain reputation in the business of business philosophy, and The Essential Drucker (TED) doesn’t disappoint. These are the greatest hits from decades of writing: an eminently quotable collection of practical advice and abstract philosophy for the humans powering the knowledge economy.

The balance of the wisdom that Drucker dispenses is aimed at practical steps that managers–and in the knowledge economy, everyone is a manager–can employ to futher their work. Focus objectives, metrics, and energy on achieving results outside the organization. Forget heroic entrepreneurs and genius innovators; focus instead on the raw ingredients of success–clear objectives, focus, hard work and perseverance–that anyone can develop. Work smart. Lead at your own level, whatever it is. And always, always keep learning.

The increasingly abstract chapters towards the conclusion of the book offer some of its most scrumptious nibbles, as Drucker traces out an intriguing (if debatable) vision for education, social enterprise, and fulfillment within post-capitalist society. Like any survey, there are chapter of TED that won’t be for everyone. Managers hungry to improve their craft will devour the first half but may find less use for musings over their place beneath the sun. Theorists will delight in the latter but face a considerable slog to reach it.

And TED is not a new book. The earliest chapters date to 1996; the most recent to 2001. For all of its familiar–and notably prescient–ideas, both ideas and language feel dated at times. The explosion of technology and the lingering effects of the great recession leave the reader to wonder how its conclusions might have been adjusted to the new reality. Faith in entrepreneurship and the indomitable human spirit are well and good. Will they survive the explosion of information that’s defined the past 15 years? That’s a question for another book.



View all posts