Adding TypeScript Support to a Babel React App

Byron Mitchell
priceline labs
Published in
7 min readNov 6, 2019
It’s me as a dog overcoming all the roadblocks.

In this post, I’m going to walk through all of the steps we used to upgrade three of our applications so that we could start mixing TS files in with our JS files. Until this past year it was very difficult to migrate to TypeScript in an existing app because if you were using something like Babel you would have to rip it out and replace it with the TypeScript compiler. Fortunately, the folks on the TypeScript team and the Babel teams worked together to create a plugin (@babel/preset-typescript) for Babel that would handle the transpilation of TypeScript into JavaScript, meaning there is no longer a need to replace Babel.

Unfortunately, adding TypeScript support to our apps without making any concessions like broken linting or unit tests was a little more involved than simply adding the babel-typescript plugin, so this post will go through all of the roadblocks I encountered and how I resolved them. If you use this as a guide, you should have an app at the end that is fully linted and supports mixed JavaScript and TypeScript.

Roadblock 1: We Were Still Using Babel 6

All of the apps that my team work on used Babel 6. Sadly, @babel/preset-typescript requires Babel 7. If you already have Babel 7 you can skip this whole section. If you don’t, you’ll have to either manually upgrade to Babel 7 by updating all dependencies in your package.json or you can use the amazing babel/babel-upgrade CLI tool to handle most of it for you. Below are the steps I took on all of our apps:

  1. Install babel/babel-upgrade globally with npm install -g babel/babel-upgrade. This is a tool that will make switching from Babel 6 to Babel 7 a relatively painless process. It misses a few important things like updating references to babel-polyfill but it was a huge time saver.
  2. Use the babel upgrade tool in the project directory with npx babel-upgrade --write. This will make most of the necessary updates to .babelrc and package.json based on what you already have in there.
  3. npm install in your project’s root directory to install the new packages and update package-lock.json.
  4. Optional (but might be necessary): babel-upgrade adds all necessary packages with ‘7.0.0’ as the version. You can manually update the modules to a more recent version if you’re having issues. We ended up updating all of the packages to ‘7.4.4’.
  5. Manually update any references to babel-polyfill to @babel/polyfill. These will typically be in the webpack configuration files.
  6. Add browserslist to package.json. This will reduce the size of the bundle by only supporting the browsers in the list.
“browserslist”: 
“last 2 chrome versions”,
“last 2 safari versions”,
“last 2 firefox versions”,
“ie >= 11”,
“last 2 edge versions”,
“> 0.5%”
]

Roadblock 2: We Needed to Add the Babel-TypeScript Plugin

Now that we had Babel 7 in our app, it was time to add the plugin that Babel uses to convert TypeScript code to JavaScript. These are essentially the same instructions that Microsoft included in their announcement blog here with a few modifications to download some types and a slightly modified tsconfig.json file.

  1. npm install --save-dev @babel/preset-typescript to install the babel Typescript plugin. This is what allows us to use Typescript without having to switch to using the Typescript compiler. Install npm install --save-dev @types/jest @types/react-dom @types/react which are type definitions that you’ll need for an application that makes use of React.
  2. Add @babel/preset-typescript to .babelrc under presets as the last item.
  3. Add a tsconfig.json file at the root of the project.
"compilerOptions": {
“baseUrl”: “src”,
// Optional, but makes it so you don’t have to use relative
// paths for imports. Add as many folders as you need.
“paths”: {
“components”: [
“./components”
]
}
// Target latest version of ECMAScript.
"target": "ESNext",
// Search under node_modules for non-relative imports.
"moduleResolution": "node",
"jsx": "react",
// Don't emit; allow Babel to transform files.
"noEmit": true,
// Import non-ES modules as default imports.
"esModuleInterop": true
},
"include": [
"src"
]
}

Roadblock 3: Webpack Didn’t Know to Load Our TypeScript Files

In order to be able to use the TS files without having to specify the extension in each import statement, we needed to let webpack know to also look at .ts and .tsx files when resolving the import paths. We also needed to let webpack know that Babel is going to handle the transpilation of .ts and .tsx files. Both of the following changes had to be made in the webpack configuration files.

  1. Add extensions option to the webpack resolve options object. E.g. extensions: [‘.ts’, ‘.tsx’, ‘.js’, ‘.json’] . This allows us to import .ts files without having to specify the .ts and it will prioritize the TypeScript files over the JavaScript ones
  2. .babel-loader module configuration needs to be updated from something like test: /\.js$/ to test: [/\.jsx?$/, /\.tsx?$/]

Roadblock 4: Jest Didn’t Know To Test Our TypeScript files

We also wanted to be able to test the new TypeScript files and collect test coverage information for them. In order to facilitate that, we had to install the babel-jest module and make several modifications to our jest.config.js file.

  1. Make sure the babel-jest module is installed npm install --save-dev babel-jest and add the babel-jest transform if it’s not already present. transform: {‘^.+\\.(js|jsx|ts|tsx)$’: ‘babel-jest’}
  2. Update the collectCoverageFrom option to include ts and tsx files. E.g. ‘src/**/*.js’ to ‘src/**/*.{js,jsx,ts,tsx}’
  3. Update testMatch option to include ts and tsx files. E.g. ‘**/__tests__/**/*.js?(x)’ to ‘**/__tests__/**/*.(js|ts)?(x)’
  4. Add or modify moduleFileExtensions to include ts and tsx files:
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
'json',
'node'
]

Roadblock 5: Our Linter Didn’t Know How To Lint Our TypeScript Files

Prior to adding in mixed TypeScript support we were using StandardJS. StandardJS is great, but it’s only for JavaScript. I made several attempts to get StandardJS to work with our new mixed JS/TS application including trying to use official instructions that StandardJS folks recommend using. However, I opted in the end to just switch back to eslint (v5) and used the eslint-config-standard and eslint-config-standard-react rules presets. Newer versions of eslint support an “override” configuration option that lets you specify a different parser for specific file types. So we opted to use babel-eslint as the default parser for all of our JS files and @typescript-eslint/parser for all of our TS files.

Unfortunately, you can’t use “extends” in “overrides” yet, so we had to rename our .eslintrc file to .eslintrc.js so that we could require the list of recommended TypeScript rules and programmatically add them as outlined in this post. I’m hoping that in the future “extends” will be supported. Here are the steps I took to enable JS/TS mixed linting:

  1. Add typescript, @typescript-eslint/eslint-plugin, and @typescript-eslint/parser modules with npm install --save-dev typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-eslint. Even though we’re using Babel to compile our Typescript to regular JavaScript, eslint requires the module to be installed in order to parse our TS files.
  2. I added a couple of scripts to the ”scripts” object in package.json. Since eslint only runs on JavaScript by default I had to pass a few — ext options.
"lint": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx src test config",
"format": "eslint --ext .js --ext .jsx --ext .ts --ext .tsx src test config --fix"

3. Below is the my full .eslintrc.js file.

// Import the recommended rules for TypeScript
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin').configs.recommended
module.exports = {
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 8,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true
}
},
extends: [
'standard',
'standard-react'
],
plugins: [
'babel',
'jest'
],
env: {
'jest/globals': true
},
rules: {
'no-console': [
'warn'
],
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/valid-expect': 'error',
'object-curly-spacing': 'off',
'no-unused-vars': 'off',
"react/prop-types": [2, { ignore: ['children'] }]
},
overrides: [
{
files: [
'**/*.ts',
'**/*.tsx'
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json'
},
plugins: [ '@typescript-eslint' ],
rules: Object.assign(typescriptEslintRecommended.rules, {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/member-delimiter-style': 'none',
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off'
})
}
],
globals: {
__DEV__: true
}
}

Roadblock 6: Eslint Doesn’t Check types

The linter was set up but there were lots of coding mistakes it didn’t account for, namely TypeScript type safety. Fortunately, since we installed typescript as a dev dependency earlier, we were able to make use of the tsc command. This command runs type checking on all TypeScript files. So to package.json I just added a new script to ”scripts”:

"check-types": "tsc --skipLibCheck"

Roadblock 7: I Really Didn’t Want to Have To Manually Run Linting and Type Checking

Having eslint and tsc to check the code is nice, but I wanted to incorporate it into the testing process for our projects. So I updated the ”test” script in ”scripts” in package.json to do linting and type checking as part of the testing process:

"test": "npm run format && npm run check-types && NODE_ENV=test jest --no-cache --silent --coverage",

Conclusion

After getting through all those roadblocks I had an app that I could start gradually converting to TypeScript. Whenever I’m working on a new feature I try to convert all of the JavaScript files I work with into TypeScript. I just rename the file and then, thanks to having eslint set up, I fix all the red squiggles I see. I hope that this post helps anyone else that is struggling with these roadblocks. Thanks for reading!

Resources

https://iamturns.com/typescript-babel/ — This is the page I first found out about babel/preset-typescript.

https://devblogs.microsoft.com/typescript/typescript-and-babel-7/ — The original blog post announcing the project.

https://babeljs.io/docs/en/babel-preset-typescript — Docs for babel-typescript.

http://www.thedreaming.org/2019/03/27/eslint-with-typescript/ — Guide to using eslint with typescript

--

--