Migrating Production JS apps to TypeScript

Philip London
Blockchain.com Engineering
5 min readApr 17, 2020
JS -> TS

I started the process of migrating blockchain.com’s web wallet application from JavaScript to TypeScript in January 2020. My goals were to increase developer productivity, decrease human errors, and improve the maintainability of our app. Our app had been in production for 1.5 years, was made up of over 2,000 JavaScript files, and contained over 125,000 lines of code.

When I began the process I noticed that, while there were tons of guides for creating a new TypeScript project, there were very few guides for migrating a production level app. This post will cover my own experience doing that, and while you may want to use parts of this post as a guide, you may find your own experience to be quite different, but hopefully this helps you avoid some of the pitfalls I came across.

Our app uses a conglomeration of different frontend libraries. Webpack, React, Redux, redux-saga, ramda, styled-components, redux-form, etc. I knew that to get the full benefits of TypeScript I’d need to make sure that we were configuring each of these libraries correctly.

Webpack

The first step to the migration was to allow our bundler to resolve and load TypeScript files. This was one of the easiest parts of the transition, which came down to installing ts-loader, updating module.rules and adding .ts and .tsx files to resolve.extensions. I tried to use awesome-ts-loader at first, but came across some performance issues. It now looks like awesome-ts-loader is archived, but still recommended by the official TypeScript guide, so I’m not sure what’s happening there.

Webpack TLDR;

// install ts-loader
npm install ts-loader
// open webpack config
code path/to/webpack.config.js
// resolve .ts and .tsx files
// load .ts and .tsx files
module.exports = {
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
},
module: {
rules: [
...
{ test: /\.tsx?$/, loader: 'ts-loader' }
...
]
}
...
}

Styled-Components

Our app has 5 different themes with over 100 color codes on each theme. I don’t always remember the name of each code and wanted TypeScript to warn me if I was using an incorrect code. It’s also important to keep themes in sync, and if a developer or designer adds a new theme they must contain all the same color codes.

After hours of research it became clear that there are two ways the community recommends implementing strongly typed themes. Styled-components documentation suggests declaring a styled-components module in a declarations file, while many resources on GitHub and stack overflow suggest creating a new styled module with ThemedStyledInterface<YourThemeHere>.

The official solution suggested by the documentation seemed more elegant and didn’t require us to worry about importing the “correct”, type safe styled module. I went back and forth trying to get the declaration file to work until I finally came across the fact that any version of @types/styled-components after 4.1.0 had broken Themes. I downgraded to v4.0.3 and declaration merging worked like a charm. I finally had type safe themes and theme autocompletion, the first big TypeScript win!

Styled-Components TLDR;

npm install @types/styled-components@4.0.3// create a declarations file for styled-components
// you can also keep all declarations in a typings/ dir
touch styled.d.ts
// styled.d.ts
import 'styled-components'
// import your default theme
import Theme from 'path/to/theme'
declare module 'styled-components' {
type ITheme = typeof Theme
export interface DefaultTheme extends ITheme {}
}
  • Don’t forget to include styled.d.ts in your tsconfig’s include

Redux

The redux documentation was unbelievably helpful and by far the clearest part of the process. However, if you weren’t a fan of the redux boilerplate before, you’re going to absolutely hate it with TypeScript. You’re going to have to define Interfaces for all of your actions and type up each slice of state, but this is of course the whole point. The cost of paying this tax upfront can be debated, but I’ve seen the payoffs almost immediately. Not only do I have more confidence in the code I’m writing, but I can also write code with the confidence that future developers will have documentation baked into the project.

Redux TLDR;

Follow the docs https://redux.js.org/recipes/usage-with-typescript/#type-checking-state

React-Redux

There seem to be a few different approaches among the community on how best to do this, and it’s made more confusing by the fact that the official docs only provide information on react-redux usage with hooks. But if you’re still primarily using class components (we are at Blockchain.com), I’ve got you covered. You will need to create at least two types.

  1. LinkDispatchPropsType: the action creator(s) connected from mapDispatchToProps
  2. LinkStatePropsType: the slices of state connected from mapStateToProps
  3. OwnProps (optional): any props passed from parent components

After combining all the PropTypes above and telling React about them we will now have a type safe component. Because of LinkDispatchToProps, and because we defined our actions in previous steps, TypeScript will autocomplete our actions and complain if a parameter is required or incorrect.

This is the most involved set up required, but it has already saved me a couple times when needing to refactor something or change a type.

EDIT: As Mark Erikson pointed out there’s actually more info in the react-redux docs than I originally thought.

Redux-Form

I’m a huge fan of forms in general and use them heavily in our app. I love the props injected by redux-form in TypeScript and the control you can achieve through form actions. Redux-form has types for v4-v7 of the library so make sure you’re installing the correct version. You’ll need to tell your component that you’re passing it redux-form props. If you’re composing your component before exporting it, you’ll need to export it as React.FunctionComponent<Props> or you will lose your types. Another note, redux-form handles onSubmit in a strange way, by changing your onSubmit prop to handleSubmit. The good news is, handleSubmit is a prop provided by InjectedFormProps, but onSubmit isn’t, so hopefully that will remind you not to use onSubmit in your form anymore.

I thought it would also be helpful to extend redux-form to only allow the form names we’re using, instead of any old string. We can also take advantage of declaration merging here to safe guard our form names and form actions.

Redux-Form TLDR;

npm install @types/redux-form@VERSION- export default reduxForm({ form: ‘FORM_NAME’ })
+ export default reduxForm<FormData, Props>({ form: ‘FORM_NAME’ })
- const MyFunctionalComponent = props => { ...
+ const MyFunctionalComponent: React.FC<InjectedFormProps<{}, Props> & Props> = props => { ...
// Optionally if you want to safe guard your form names:touch redux-form.d.ts// redux-form.d.tsimport {
ConfigProps
} from 'redux-form'
export type WalletFormType =
| '@SEND.BCH.FORM'
| '@SEND.BTC.FORM'
| '@SEND.ETH.FORM'
| '@SEND.XLM.FORM' ...
declare module 'redux-form' {
interface CustomConfigProps<FormData = {}, P = {}> extends ConfigProps {
form: WalletFormType
}

export function reduxForm<FormData = {}, P = {}>(
config: CustomConfigProps<FormData, P>
): FormDecorator<FormData, P, Partial<CustomConfigProps<FormData, P>>>
}

Conclusion and Next Steps

Using TypeScript has made me think more holistically about the code I am writing and the user experiences I am creating. I hope that if you’re in the process of migrating a production level app from JS to TS you have found parts of this post helpful.

Since starting the migration we’ve managed to convert 20% of our app to TypeScript in Q1. I’m hoping to have migrated 80% by the end of Q4. 🚀

--

--