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-upgrade
globally 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-polyfill
but 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.babelrc
andpackage.json
based on what you already have in there. npm install
in your project’s root directory to install the new packages and updatepackage-lock.json
.- 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’
. - Manually update any references to
babel-polyfill
to@babel/polyfill
. These will typically be in thewebpack
configuration files. - Add
browserslist
topackage.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-typescript
to 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/react
which are type definitions that you’ll need for an application that makes use of React.- Add
@babel/preset-typescript
to.babelrc
underpresets
as the last item. - 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.
- Add
extensions
option to thewebpack
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 - .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-jest
module is installednpm install --save-dev babel-jest
and add thebabel-jest
transform if it’s not already present.transform: {‘^.+\\.(js|jsx|ts|tsx)$’: ‘babel-jest’}
- Update the
collectCoverageFrom
option to includets
andtsx
files. E.g.‘src/**/*.js’ to ‘src/**/*.{js,jsx,ts,tsx}’
- Update
testMatch
option to includets
andtsx
files. E.g.‘**/__tests__/**/*.js?(x)’ to ‘**/__tests__/**/*.(js|ts)?(x)’
- Add or modify
moduleFileExtensions
to includets
andtsx
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:
- Add
typescript
,@typescript-eslint/eslint-plugin
, and@typescript-eslint/parser
modules 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,eslint
requires the module to be installed in order to parse our TS files. - I added a couple of scripts to the
”scripts”
object inpackage.json
. Sinceeslint
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.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