GigPin Engineering Blog

Moving to TypeScript

June 07, 2018

Moving a large React application written in vanilla JS (actually ES6 with some additional plugins) to Typescript has benefits, but breaks a lot of things that were working just fine. The application was created with create-react-app (we didn’t want to bother with configuring our own app) and we did not want to eject, so solution had to be found.

Adding TypeScript

Our initial attempt was to take webpack.config.js from react-scripts package, modify it to use TS and then replace it. This could be done manually, through shell script and as a build step.

Unfortunately, this was brittle and we had to keep up with CRA scripts. We decided to go with react-scripts-ts which was much more stable and it was supported by the community.

But we wanted to add additional stuff like CSS Next without ejecting. An open source project named react-app-rewired offers customizing Webpack configuration in a really straightforward way. We continued using react-scripts-ts which meant code stays the same. In the end this was a really simple change, not to mention a lot more stable one. All we needed to do is to replace npm scripts with:

{
  "scripts": {
    "start": "react-app-rewired start --scripts-version react-scripts-ts",
    "build": "react-app-rewired build --scripts-version react-scripts-ts"
  }
}

Additionaly Storybook was hard to setup in TS, but nowadays they have documentation on how to do it on their website.

Errors, lots of errors

TypeScript can be unforgiving, but it can be customized via tsconfig.json. Since there were no type annotations before and we are using imports from untyped packages, here are some recommended default settings:

  • allowSyntheticDefaultImports - allows you to write import React from 'react' instead of import * as React from 'react'
  • noImplicitAny - since everything is untyped, do this or change all the files

Even though awesome-typescript-loader can support js extension it is strongly recommended that all files are renamed to ts/tsx unless they are some helper pure js files.

Additionally, the following code won’t work:

const items = []; // <- never[]
items.push({a: 1});
items.map(item => item.a);

The array has type never[] which meant that we need to type it in order to access properties in a safe manner. The quickest way is to simply replace the declaration with:

const items: any[] = [];

Accessing properties on window doesn’t work since TS cannot recognize them, but that can be replaced with window['property'].

It is strongly suggested that you add @types for all libraries which have them, it helps a lot when discovering complex parameters for functions for member methods.

Biggest benefit - typed API

Our backend is classic REST which means complicated entities in response. Guessing which properties existing under groups.messages[i] few months after the initial code was written by you or other colleague is really annoying. After all, we spend most of our time reading code, not writing it.

So types are autogenerated from our backend and types are added incrementally to the code. This helps a lot, especially when reading unknown code.

Benefit - required and unused properties

Components are refactored as requirements change or as a side effect of other changes. Removing a property from component in ES6 has no effect in our IDE, but if the component is typed, we will have information in all places the component has been used.

Removing a property - get a warning about unused property. Adding a new one - get a warning about missing property.

Introducing and removing code is essential part of refactoring which happens in all codebases, especially if you are building a product for longer time.

Conclusion

All in all, incremental switching to TypeScript helps a lot with aging code. There is always an escape hatch in the form of any when we don’t want to bother with typing.