Adding TypeScript Support to a Babel React App

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:
- Install
babel/babel-upgradeglobally withnpm 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 tobabel-polyfillbut it was a huge time saver. - Use the babel upgrade tool in the project directory with
npx babel-upgrade --write. This will make most of the necessary updates to.babelrcandpackage.jsonbased on what you already have in there. npm installin your project’s root directory to install the new packages and updatepackage-lock.json.- Optional (but might be necessary):
babel-upgradeadds 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’. - Manually update any references to
babel-polyfillto@babel/polyfill. These will typically be in thewebpackconfiguration files. - Add
browserslisttopackage.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.
npm install --save-dev @babel/preset-typescriptto install the babel Typescript plugin. This is what allows us to use Typescript without having to switch to using the Typescript compiler. Installnpm install --save-dev @types/jest @types/react-dom @types/reactwhich are type definitions that you’ll need for an application that makes use of React.- Add
@babel/preset-typescriptto.babelrcunderpresetsas the last item. - Add a
tsconfig.jsonfile 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.
- Add
extensionsoption to thewebpackresolveoptions 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 - .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.
- Make sure the
babel-jestmodule is installednpm install --save-dev babel-jestand add thebabel-jesttransform if it’s not already present.transform: {‘^.+\\.(js|jsx|ts|tsx)$’: ‘babel-jest’} - Update the
collectCoverageFromoption to includetsandtsxfiles. E.g.‘src/**/*.js’ to ‘src/**/*.{js,jsx,ts,tsx}’ - Update
testMatchoption to includetsandtsxfiles. E.g.‘**/__tests__/**/*.js?(x)’ to ‘**/__tests__/**/*.(js|ts)?(x)’ - Add or modify
moduleFileExtensionsto includetsandtsxfiles:
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:
- Add
typescript,@typescript-eslint/eslint-plugin, and@typescript-eslint/parsermodules withnpm 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,eslintrequires the module to be installed in order to parse our TS files. - I added a couple of scripts to the
”scripts”object inpackage.json. Sinceeslintonly 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.recommendedmodule.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

