Published 2017-5-12
Why to use TypeScript with React & Redux

Typescript and React & Redux

Let's talk a little bit about one really interesting language based on JavaScript - it's TypeScript.

What it brings

Type system for JavaScript

  • Type safety - for functions, variables, classes
  • Easy refactoring of typed code
  • Types can cover many cases you would normally want to write Unit tests

Powerful type system - better than Java has.

  • This mat be little subjective but point is that you need to write less and you will get more
  • You can specify wider type and then specify it more by using condition. See example:
function getParam(parameter: number | string): string {
  if (typeof parameter === 'number') {
    return parameter; // error  - Type 'number' is not assignable to return type 'string'
  }
  return parameter; // ok - numberOrStringParam: string
}

Alternatives (flow?)

If you are not convinced with TypeScript, nt sure why you would not be, there is still alternative - flow

Main advantage of Flow should be more soundless checking without need to specify types. But TypeScript also got similar features and became relatively clever in type inference.

See nice table of differences: https://github.com/niieani/typescript-vs-flowtype

  • TypeScript is written in TypeScript (which is compiled to JavaScript and may be run easily in every tool)
  • Flow is written in OCaml (because of this it's more complex to integrate with tools)

Real world experience is that TypeScript is more mature than Flow (as of 2017, update: still in 2018). There is better tooling and more types definitions for packages.

Let's start with TypeScript

Note that you can benefit from TypeScript setup even if you are working with plain JavaScript. TypeScript compiler itself is much more clever than plain JS alternatives.

# Install typescript to project and save into package.json
npm i typescript --save-dev

# Create tsconfig.json typescript configuration file
tsc --init

tsconfig.json contains all settings for typescript compiler. You can compile also .js files with option "allowJs": true

Webpack setup

Add TypeScript webpack loader (https://github.com/TypeStrong/ts-loader):

Simplest TypeScript webpack webpack.config.js:

module.exports = {
  entry: './main.ts',
  output: {
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    loaders: [
      { test: /\.tsx?$/, loader: 'ts-loader' }
    ]
  }
}

React Create App (TypeScript)

Alternatively you can accelerate your development start with React CLI. Sadly, there is no official support for TypeScript, but there is maintained fork, so that you can generate new application with it.

See https://github.com/wmonk/create-react-app-typescript

npm install -g create-react-app

create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start

Redux

Redux contains it's own types inside library, so these types will be installed with Redux itself - https://github.com/reactjs/redux/blob/master/index.d.ts

To gain great benefits of TypeScript, we need to make sure, we do not loose any type after connect in Container, or when working with state in Reducers.

Please note that there might be different Redux setups and this is just one variation, which worked well for me.

1) Actions

Configure all actions in one single object, place it in actions/types.ts:

export type Action =
  | { type: '@@router/LOCATION_CHANGE'; payload: { pathname: string; search: string; hash: string } }
  | { type: 'EXPAND_MENU'; menuId: number }
  | { type: 'COLLAPSE_ALL_MENUS' }
  | ...

Now everywhere where we will be defining actions, we will expect this object contain all possible actions.

Specific actions eg. in actions/menu.ts would looks like following:

import { Action } from './types';

export const expandMenu = (menuId): Action => ({
  type: 'EXPAND_MENU'
});

export const collapseAllMenus = (): Action => ({
  type: 'COLLAPSE_ALL_MENUS'
});

2) Reducers (with fully typed store state)

Reducers will also contain whole State typed object.

import { Action } from '../actions/types';

export interface IExpandedMenus {
  [id: string]: boolean;
}

export const expandedMenus = (state: IExpandedMenus = {}, action: Action): IExpandedMenus => {
  switch(action.type) {
    case 'EXPAND_MENU':
      return {
        ...state,
        [action.menuId]: true
      }
    case 'COLLAPSE_ALL_MENUS':
      return {};
    default:
      return state;
  }
};

Now, we have connected Action and Reducer, try to make some changes to global action type, you should directly see that reducer need to be updated too. This will eliminate many possible integration and refactoring issues.

What is also important is to create global store State and Dispatch.

export interface IState {
  expandedMenus: IExpandedMenus;
  router: ReactRouterState;
  // All other reducer types/ type combined trees
}

export type IDispatch = (action: Action) => void;

3) Containers (with fully typed connects)

After we have actions and reducer, we are missing just one thing and it is connection to real React component.

This is done with following code:

import { connect } from 'react-redux';
import { IState, IDispatch } from '../reducers/types';
import { expandMenu, collapseAllMenus } from '../actions/menu';
import { IOwnProps, IStateProps } from Component;

const mapStateToProps = (state: IState, ownProps: IOwnProps): IStateProps => ({
  expandedMap: state.expandedMenus
});

const mapDispatchToProps = (dispatch: IDispatch): IDispatchProps => ({
  onExpandMenu (id: string) {
    dispatch(expandMenu (id));
  },
  onCollapseAll() {
    dispatch(collapseAllMenus());
  }
});

export default connect(mapStateToProps, mapDispatchToProps)(Component);

Component needs to have defined and exported required interfaces.

And that's all.

Not so simple, but when integrated properly, you will gain great benefits of bulletproof codebase and you can focus on building functionality instead of finding bugs!

;