DEV Community

bright inventions
bright inventions

Posted on • Edited on • Originally published at brightinventions.pl

Using TypeScript with Redux

Recently we've checked what benefits TypeScript adds in our React project. Let's now follow the typical path the front-end project takes nowadays and add Redux into the mix. Unfortunately, by default Redux is not really type safe as it is in most cases presented as switches over actions with arbitrary payloads. But TypeScript is a powerful beast and with a bit of care we can make our Redux code not only type safe, but also much cleaner and more readable.

Redux + TypeScript

Basic typing

Let's dissect the classical To Do list example from the "Core Concepts" introductory Redux guide's page step-by-step. The global app's state originally looks as follows:

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}
Enter fullscreen mode Exit fullscreen mode

It is pretty straightforward to create a type definition for this state:

interface Todo {
    text: string
    completed: boolean
}

type VisibilityFilter = 'SHOW_COMPLETED' | 'SHOW_ALL'

interface AppState {
    todos?: Todo[]
    visibilityFilter?: VisibilityFilter
}
Enter fullscreen mode Exit fullscreen mode

Note that AppState's properties are optional as the state might initially be empty and we need to handle it in our code and thus reflect it in our type definition.

So far so good. Now Actions – they initially look like this:

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
Enter fullscreen mode Exit fullscreen mode

Let's for now use the typing provided by redux library itself, specifically AnyAction that only enforces the type property to be set:

const actions: AnyAction[] = [
    { type: 'ADD_TODO', text: 'Go to swimming pool' },
    { type: 'TOGGLE_TODO', index: 1 },
    { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
]
Enter fullscreen mode Exit fullscreen mode

Better than nothing, we can't have an Action without a type at least and we can put arbitrary data into it.

Now let me jump to Reducers. Reducers in the original example are implemented this way:

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map(
        (todo, index) =>
          action.index === index
            ? { text: todo.text, completed: !todo.completed }
            : todo
      )
    default:
      return state
  }
}

// top-level reducer
function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's use our State and Action type definitions as a first step:

function visibilityFilter(state: VisibilityFilter = 'SHOW_ALL', action: AnyAction): VisibilityFilter {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state: Todo[] = [], action: AnyAction): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map(
        (todo, index) =>
          action.index === index
            ? { text: todo.text, completed: !todo.completed }
            : todo
      )
    default:
      return state
  }
}

// top-level reducer
function todoApp(state: AppState = {}, action: AnyAction): AppState {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that all reducers, regardless of on which level they exist, share the same generic signature:

type Reducer<S> = (state: S, action: AnyAction) => S;
Enter fullscreen mode Exit fullscreen mode

This is exactly the type that the built-in Redux typings provide, so we can use it in our code directly if we have an urge to generalize it more for some reason.

Can we do more?

All we have done so far was to add the basic type definitions that could prevent us from a set of typos. But looking at the reducers, we're far from an actual type safety as AnyAction-typed actions – as this any in the name suggests – does not give the TypeScript compiler any clue what payload the particular action type actually carries. We might still make a typo while accessing action's payload properties, assume its wrong type or use the properties of action of another type by mistake and TypeScript has no tool to warn us in this case:

function visibilityFilter(state: VisibilityFilter = 'SHOW_ALL', action: AnyAction): VisibilityFilter {
    if (action.type === 'SET_VISIBILITY_FILTER') {
        return action.fliter // note the typo here. TypeScript is unable to detect the problem here
    } else {
        return state
    }
}
Enter fullscreen mode Exit fullscreen mode

But all hope is not lost yet. TypeScript has a powerful feature of Discriminated Unions and even though this name sounds like some kind of organization of a social minority, it might help us a lot here.

Let's create detailed type definitions for each kind of action we support:

interface AddTodoAction extends Action {
    type: 'ADD_TODO'
    text: string
}

interface ToggleTodoAction extends Action {
    type: 'TOGGLE_TODO'
    index: number
}

interface SetVisibilityFilterAction extends Action {
    type: 'SET_VISIBILITY_FILTER'
    filter: VisibilityFilter
}
Enter fullscreen mode Exit fullscreen mode

Note that we extend Redux-provided Action now which only specifies a type property. AnyAction type we used before permits any property to exist in its implementations, so it defeats our desired type safety. Another interesting bit here is that we specify the type property with a literal, making it a good candidate for the discriminator of our discriminated union. Here is the union itself:

type TodoAppAction = AddTodoAction | ToggleTodoAction | SetVisibilityFilterAction
Enter fullscreen mode Exit fullscreen mode

Now let's replace AnyAction in our reducers with TodoAppAction and let's see what happens now:

function visibilityFilter(state: VisibilityFilter = 'SHOW_ALL', action: TodoAppAction): VisibilityFilter {
    if (action.type === 'SET_VISIBILITY_FILTER') {
        return action.filter
    } else {
        return state
    }
}

function todos(state: Todo[] = [], action: TodoAppAction): Todo[] {
    switch (action.type) {
        case 'ADD_TODO':
            return state.concat([{text: action.text, completed: false}])
        case 'TOGGLE_TODO':
            return state.map(
                (todo, index) =>
                    action.index === index
                        ? {text: todo.text, completed: !todo.completed}
                        : todo
            )
        default:
            return state
    }
}

function todoApp(state: AppState = {}, action: TodoAppAction): AppState {
    return {
        todos: todos(state.todos, action),
        visibilityFilter: visibilityFilter(state.visibilityFilter, action)
    }
}
Enter fullscreen mode Exit fullscreen mode

If you use a TypeScript-aware IDE (like WebStorm, for example), you might notice that the action properties are now properly colored and the code completion works exactly as we might have dreamt. Let's now try our typo example again:

TypeScript properly checks Action payload types

A victory for humanity, isn't it?

Connected Components

One more place we might feel betrayed by Redux is where it actually binds into React components. Let's use a bit simplified example from the Redux guide again:

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'

// the component itself

const Link = ({active, children, onClick}) => {
  if (active) {
    return <span>{children}</span>
  }

  return <a href="" onClick={onClick}>
    {children}
  </a>
}

// redux binding

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(mapStateToProps, mapDispatchToProps)(Link)
Enter fullscreen mode Exit fullscreen mode

Let's define the types for the Link component's props in the straightforward way:

interface LinkProps {
    active: boolean
    onClick: () => void
}

const Link: React.StatelessComponent<LinkProps> = props => {
  if (props.active) {
    return <span>{props.children}</span>
  }

  return <a href="" onClick={props.onClick}>
      {props.children}
  </a>
}
Enter fullscreen mode Exit fullscreen mode

This works fine, but this way we can't go further into mapStateToProps or mapDispatchToProps as these methods compose our final props object from independent parts and we either leave it untyped or describe it with some kind of workarounds like Partial<LinkProps> to at least give us the basic checks:

Workaround on mapStateToProps might help a bit

Note we don't have a proper type for ownProps here, too, and "for convenience" we used any. Also, this approach let us return onClick property here which we clearly expect to be returned only from mapDispatchToProps in this case. Let's be explicit about it and split our props into three separate types: one for own props (passed from the parent component), one for state-based props and one for dispatch-based props:

interface LinkOwnProps {
    filter: VisibilityFilter
}

interface LinkStateProps {
    active: boolean
}

interface LinkDispatchProps {
    onClick: () => void
}

type LinkProps = LinkOwnProps & LinkStateProps & LinkDispatchProps
Enter fullscreen mode Exit fullscreen mode

And now our Redux bindings are fully type safe:

const mapStateToProps = (state: AppState, ownProps: LinkOwnProps): LinkStateProps => {
    return {
        active: ownProps.filter === state.visibilityFilter
    }
}

const mapDispatchToProps = (dispatch: Dispatch<AppState>, ownProps: LinkOwnProps): LinkDispatchProps => {
    return {
        onClick: () => {
            dispatch(setVisibilityFilter(ownProps.filter))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For me the type safety given by proper TypeScript definitions made Redux great again. How about you?

Originally published at brightinventions.pl

By Adam Bar, The Web Guy @ Bright Inventions
Personal blog Twitter Email Github

Top comments (0)