Porting a React and Redux application to TypeScript

I’m increasingly becoming interested in TypeScript, Microsoft’s JavaScript superset. It provides a strongly-typed variant of JS, one that ultimately compiles down to plain JavaScript via a compiler tool like Webpack.

While TypeScript’s official website does have pretty good documentation, there is no real substitute for writing actual code. As such, this is my experience porting a React and Redux application over to TypeScript.

The application I moved is thewidgetcompany, a sample application written as part of Bytesized’s free Redux course, Redux Manufacturing. It’s a fairly simple collection of React components and a few Redux actions: everything is contained locally and there are no external API dependencies. All in all, it’s a good example application to use as a guinea pig for our TypeScript experiments.

It may be useful to briefly summarize thewidgetcompany as an application, before we dive into how to port it into TypeScript. The application contains a number of components, each interfacing with different parts of the application state to create fictional widgets, which are ultimately packaged and shipped. The “flow” of the application can be simplified as: Raw Materials -> Create Widgets -> Package Widgets -> Ship Widgets. A number of the components simply provide a buttons like “Package Order” or “Ship Order”, and send a message to application state to modify the number of materials, packaged orders, etc. The actual logic in the application remains the same throughout our TypeScript migration–instead, we focus on defining types and interfaces around those existing objects and actions, to ensure that the functionality is bug-free and consistent.

Extensions

TypeScript files are indicated by the extension .ts – if the file is a JSX/React file, you can also use .tsx. Because this is a React application, we’ll move our code over to .tsx.

We could manually rename all of our JS files in our src directory, but the command-line always has a solution for these kind of problems. In Bash-like systems, batch renaming every .js file to .tsx looks like this:

find . -name "*.js" -exec rename 's/\.js$/.tsx/' '{}' \;

One amazing thing about TypeScript is that it transparently supports JavaScript files. While our source is now comprised of .tsx/TypeScript files, it can still be pure JavaScript and be 100% functional. This makes it super easy to transition our code over piece by piece.

Compilation

Of course, our application is only configured to handle JavaScript files; it has no clue what TypeScript is or what TypeScript files look like.

This application was originally based on create-react-app–we’ve used the “eject” functionality to create a self-contained React application, with all of our dependencies declared. create-react-app uses Webpack, so we need to change parts of our Webpack config to handle .tsx files. This configuration is based heavily on TypeScript’s own documentation: “React and Webpack”

Update: just-boris on Reddit notes that Microsoft’s own TypeScript-React-Starter repository on GitHub provides a solution for using TypeScript inside of create-react-app.

To begin, we need to install the relevant TypeScript dependencies in our project:

npm install --save-dev typescript awesome-typescript-loader source-map-loader
npm install --save @types/react @types/react-dom

With our dependencies declared, we need to define TypeScript’s config file: tsconfig.json. Our config file will be similar to the one provided by the TypeScript documentation, but with a few modifications:

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es6",
    "jsx": "react",
    "allowJs": true
  },
  "include": [
    "./src/**/*"
  ]
}

We’ve updated the target key to es6, which will allow ES6-style JavaScript code. Because this code still goes through the rest of our Webpack compilation process, we can write ES6-like code and have it pass through the existing ES6 configuration that create-react-app provides.

We’ll also add allowJs, which will be set to true. This functionality is what allows us to use pure JavaScript code in our TypeScript files. This is an easy way to slowly transition your code over into TypeScript: if you’re starting fresh with a 100% TypeScript project, you might want to reconsider this.

The most important parts of tsconfig.json are the include and outDir directories: it takes in the src directory (and all the files inside of it, recursively) and outputs to the dist folder. This works for how this ejected create-react-app is configured: you may need to reconfigure these paths for your application.

Finally, we need to update the Webpack configuration itself. For thewidgetcompany, this is config/webpack.config.dev.js.

In the extensions section, we need to add two additional extensions: .ts and .tsx. The final version, on line 77, looks like this:

extensions: ['.ts', '.tsx', '.js', '.json', '.jsx', '']

Additionally, the TypeScript loader needs to be integrated in the loaders section of the config. awesome-typescript-loader will match on .ts and .tsx files, and is added on line 166:

{ test: /\.tsx?$/, loader: "awesome-typescript-loader" }

Finally, we need to update the “entry point” for our application. In thewidgetcompany, this is found in config/paths.js: depending on how your application is configured, this might be found in the Webpack config file as well. Line 71 is updated to:

appIndexJs: resolveApp('src/index.tsx')

With all this set up, we should be able to start our application with npm start and see our first error:

Error in [at-loader] ./src/index.tsx:1:19 
  TS7016: Could not find a declaration file for module 'react'. 
  '/Users/kristian/src/bytesized/thewidgetcompany-eject/node_modules/react/react.js' implicitly has an 'any' type.

Linting

Now the migration process to TypeScript begins. I highly recommend at this point that you integrate a TypeScript plugin into your text editor–I’ve been using Visual Studio Code which works with TypeScript out of the box, and it’s been a great experience. Having TypeScript validation in your text editor allows you to fix various errors without needing to switch back to your build process after each save, to ensure that what you did actually fixed the issues.

If you want to quickly learn a fairly canonical way of writing TypeScript, you might consider adding tslint, a TypeScript linting tool for your code:

npm install --save-dev tslint tslint-loader tslint-react

tslint requires a config file, tslint.json:

{ 
  "extends": ["tslint:recommended", "tslint-react"]
}

Back in our Webpack configuration, in the loaders section:

// Typescript linting
{
  enforce: "pre",
  loader: "tslint-loader",
  test: /\.tsx?$/
},

Restarting our build process will now ensure that .ts and .tsx files pass through tslint before proceeding with the standard TypeScript compilation process.

You might also want to include tslint in your editor–in Visual Studio Code, I had success with vscode-tslint.

Migration

We now have the infrastructure to fully move our application over to TypeScript. Because there will be a number of warnings and errors, I’ll introduce some of the changes and TypeScript concepts in more of a list format, since we’ll be jumping around the application, changing code as needed.

Imports

The default import style used in many React projects (import React from 'react') is not immediately compatible with how TypeScript handles imports. This error manifests itself as the first error we saw above, namely:

Error in [at-loader] ./src/index.tsx:1:19 
    TS7016: Could not find a declaration file for module 'react'. '/Users/kristian/src/bytesized/thewidgetcompany-eject/node_modules/react/react.js' implicitly has an 'any' type.

A previous version of this guide encouraged changing the import style–TypeScript now fixes the need to replace these definitions, via the --allowSyntheticDefaultImports configuration option. You can set this in your tsconfig.json file and remove this error entirely, without needing to change how you import packages.

Interfaces and Types

The most immediate win for most applications moving to TypeScript is defining centralized interfaces and types. Interfaces and types allow you to explicitly define how data will look across your application: it gives your code the strongly-typed definition that the name TypeScript suggest.

If you’re building a more complex application, you should lean on TypeScript’s classes instead. In thewidgetcompany, the application state is very basic, and this would probably be an over-optimization.

There are a number of things in our application that can be defined with interfaces and types. For instance, our entire application state tree:

// types.tsx
interface IApplicationState {
  error?: string;
  failed: number;
  materials: IMaterialSet;
  orders: IOrder[];
  packaged: number;
  shipped: number;
  widgets: IWidget[];
}

interface IMaterial {
  count: number;
}

interface IMaterialSet {
  dowel: IMaterial;
  screw: IMaterial;
  wheel: IMaterial;
}

interface IOrder {
  created: number;
  widgets: number;
}

interface IWidget {
  created: number;
}

export {
  IApplicationState,
  IMaterial,
  IMaterialSet,
  IOrder,
  IWidget,
};

There are a number of simple types in TypeScript–above, we used string and number. A value can be optional, too: error?: string in IApplicationState indicates that the error string can be set to null. Finally, by defining our own interfaces, we can re-use them in defining other interfaces: orders: IOrder[] in IApplicationState indicates that we expect an array of orders, of type IOrder.

The IApplicationState interface is used to ensure that our application state always conforms to this structure, regardless of where it is used in the codebase. For instance, our Redux reducers (vastly condensed in the below code sample) can benefit from making sure that a consistent application state is always returned from functions:

import {
  IAction,
  IApplicationState,
} from "./types";

const initialState: IApplicationState = {
  error: null,
  failed: 0,
  materials: {
    dowel: { count: 2 },
    screw: { count: 8 },
    wheel: { count: 3 },
  },
  orders: [],
  packaged: 0,
  shipped: 0,
  widgets: [],
};

const orderMaterials = (state: IApplicationState): IApplicationState => {
  const { materials } = state;
  const { dowel, screw, wheel } = materials;
  return Object.assign({}, state, {
    error: null,
    materials: {
      dowel: { count: dowel.count + 10 },
      screw: { count: screw.count + 10 },
      wheel: { count: wheel.count + 10 },
    },
  });
};

export default function appState(state = initialState, action: IAction): IApplicationState {
  switch (action.type) {
    case ORDER_MATERIALS:
      return orderMaterials(state);
    default:
      return state;
  }
}

The eagle-eyed among you might have noticed IAction, which didn’t exist in the original types.tsx definition. Below, we’ll add IAction, and begin to use it in our Redux actions:

// types.tsx
interface IAction {
  type: string;
  message?: string;
  order?: IOrder;
}

export { 
  // ...
  IAction,
}

// actions.tsx
import { IAction } from "./types"

export function orderMaterials(): IAction {
  return {
    type: ORDER_MATERIALS,
  };
}

Type definitions

In thewidgetcompany, we use the Redux middleware redux-thunk. redux-thunk provides a “thunk” action that we can use to defer actions. In our Redux actions, this means that there is another type of action that doesn’t exactly fit our IAction interface.

The solution is two-fold: first, we need to import the built-in type definitions for the redux-thunk package, and integrate them as type definitions in our actions. Second, we need to build two types: IDispatchAction, which models how an action is passed to our application as a prop, and IThunkedAction, which defines a function type that looks like a “thunked” action should, in our actions.tsx file.

To begin, we’ll install the types for redux-thunk. You may have noticed at the beginning of this tutorial that we used npm install --save @types/react: this notation installs the relevant type definitions for the specified package. In a much similar way, we’ll do this for redux-thunk:

npm install --save @types/redux-thunk

This adds the type definitions for redux-thunk to our project: our TypeScript configuration should pick it up automatically. For the extra curious, you can see what the type definitions for redux-thunk look like at the project’s index.d.ts.

In our code, we’ll define IDispatchAction, as well as import the new Dispatch type from redux-thunk. This will be used to strongly-type an action passed to a component as a prop:

type IDispatchAction = () => void;

In our App component, we’ll define an IProps interface, which will model how the passed-in props for the component should look. A Component expects a type definition in the format Component<Props, State> – since the App component does not use any component-level state, we’ll set the type definition to Component<IProps, {}>:

import {
  IApplicationState,
  IDispatchAction,
} from "./types";

interface IProps extends IApplicationState {
  attemptWidgetCreation: IDispatchAction;
  checkOrderForPackaging: IDispatchAction;
  generateOrder: IDispatchAction;
  orderMaterials: IDispatchAction;
  shipOrder: IDispatchAction;
}

class App extends Component<IProps, {}> {
  public render() {
    // ...
  }
}

const mapStateToProps = (state: IApplicationState) => state;
const mapActionCreators = {
  attemptWidgetCreation,
  checkOrderForPackaging,
  generateOrder,
  orderMaterials,
  shipOrder,
};

export default connect(
  mapStateToProps,
  mapActionCreators,
)(App);

IThunkedAction, on the actions.tsx side, uses the Dispatch action provided by redux-thunk to strongly-type a thunked action:

import {
  ADD_WIDGET,
  DOWELS_NEEDED,
  PRESENT_ERROR
  SCREWS_NEEDED,
  WHEELS_NEEDED,
} from "./constants";

import { Dispatch } from "redux";

import {
  IAction,
  IApplicationState,
  IThunkedAction,
 } from "./types";

export function attemptWidgetCreation(): IThunkedAction {
  return (dispatch: Dispatch<IApplicationState>, getState: () => IApplicationState) => {
    const { materials } = getState();
    const { dowel, screw, wheel } = materials;

    if (dowel.count >= DOWELS_NEEDED && screw.count >= SCREWS_NEEDED && wheel.count >= WHEELS_NEEDED) {
      dispatch(addWidget());
    } else {
      dispatch(presentError("Not enough materials to create a widget"));
    }
  };
}

export function addWidget(): IAction {
  return {
    type: ADD_WIDGET,
  };
}

export function presentError(message: string): IAction {
  return {
    message,
    type: PRESENT_ERROR,
  };
}

Prop interfaces

In our App component, we defined IProps, which modeled what the props passed to our component should look like. In each component in thewidgetcompany, an IProps interface is defined, though they are all less complex than the initial App props:

// Inventory.tsx
import { IWidget } from "./types";

interface IProps { widgets: IWidget[]; }

class Inventory extends Component<IProps, {}> {
  public render() {
    const { widgets } = this.props
    return (
      <div>
        <h2>Inventory</h2>
        <h4>{widgets.length} widgets in inventory</h4>
      </div>
    );
  }
}

// Shipping.tsx
interface IProps {
  packaged: number;
  shipped: number;
  shipOrder: () => void;
}

class Shipping extends Component<IProps, {}> {
  public render() {
    const { packaged, shipped, shipOrder } = this.props;
    return (
      <div>
        <h2>Shipping</h2>
        <h4>{shipped} orders shipped</h4>
        {packaged ? <button onClick={shipOrder}>Ship order</button> : <h4>No orders to ship</h4>}
      </div>
    );
  }
}

These interfaces are specific enough to each component that I’ve kept them in their respective components: if you start to see a pattern emerge in your own code, you might consider pulling these interfaces out into types.tsx.

Conclusion

At the end of our application’s transition to TypeScript, it’s worth asking: Was it worth it?

Consider the following scenario: our “Order” object, which consists of a created date and the number of widgets required to fill the order, now requires an addition customerId identifier to tie it to the customer that ordered it. In a vanilla JavaScript application, we might not feel confident updating the various places that orders are passed and queried. In our TypeScript application, the IOrder interface that we’ve defined gives us confidence that our code will work. If we update our IOrder interface to include a customerId field, we’ll immediately be aware of what pieces of code aren’t satisfying that requirement:

// types.tsx
interface IOrder {
  created: number;
  customerId: string;
  widgets: number;
}

// reducers.tsx
const generateOrder = (state: IApplicationState): IApplicationState => {

  // ERROR: Property 'customerId' is missing in type '{ created: number; widgets: number; }'.
  const newOrder: IOrder = {
    created: Date.now(),
    widgets: Math.floor(Math.random() * 10) + 1,
  };
  // ...
};

Writing TypeScript brings the best part of type-safety to your web apps: knowing that something will break, before it actually does. To answer our previous question, our transition to TypeScript was worth it–we’ve reached a point where we can confidently continue to build on our application.

The complete source code to accompany this blog post can be found on Gitlab: bytesized/thewidgetcompany-typescript.

Bytesized offers technical training for companies. If you or your company want to take a deeper dive into React.js or Redux, including hands-on workshops, lectures, and take-home exercises, contact us to schedule training. We’re excited to work with you.

Leave a Reply

Your email address will not be published. Required fields are marked *