Introduction
For the last couple of years I’ve been writing my front end code in TypeScript. As a new member of the SalesScreen team, I had a goal to introduce TypeScript to my new team and show them how it has helped me in writing better code. SalesScreen has a fairly large front end code base, (approximately 170k lines of JS(X), across 2300 files). Rewriting such a large code base to TypeScript is not done in a short amount of time, and we will most likely have a mix of JavaScript and TypeScript files for a long time ahead. Luckily, or naturally one might say, we use Babel to transform our code so we use modern ECMAScript features and still have wide browser support. With the release of Babel 7 and a new plugin, @babel/plugin-transform-typescript, earlier this year, supporting TypeScript in your build has become super easy. In this post I will try to describe why using Babel and TypeScript together is a good idea, as well as show how easy it is to incorporate in your existing Babel configuration.
Why should I use Babel for TypeScript
You might ask yourself this question. TypeScript comes with its on compiler, the TypeScript Compiler (tsc). Both support writing modern ECMAScript and outputting ES5 for older browsers. However, the TypeScript compiler is the only one which can do type-checking.
Before Babel 7, the most common method to bundle a code base mixing JavaScript and TypeScript, was to use Webpack, together with the babel-loader and one of the many TypeScript loaders out there. Choosing the correct loader for your project can be overwhelming by it self, just take a look at the README for the most popular loaders: ts-loader and awesome-typescript-loader. This process combined the two compilers and merged the JavaScript outputted from compiling your TypeScript with the pure JavaScript in your code base, before piping it all through Babel to get ES5 code with all the necessary polyfills.
Getting this configuration right, without imposing unnecessary build time, is not easy, and I would not be surprised if this is just to large of a threshold for many developers wanting to introduce TypeScript in their projects. Babel 7 to the rescue. With the new @babel/plugin-transform-typescript we can simplify this process, by decoupling the TypeScript Compiler from the bundling pipeline, and let Babel do all the bundling work. As mentioned earlier, the TypeScript Compiler is still the only one that can perform type-checking. So we still need it, but only when you know you want to do type-checking. This gives us two main advantages:
- It’s super fast. Since Babel does not support type-checking, all it does to your TypesScript code is striping it of TypeScript syntax, leaving behind plain old JavaScript.
- Do type-checking on demand. Just as you know that your unit-tests will most likely fail when you do alterations to your code, you know that your code probably isn’t type safe anymore. But why should that stop you from coding on and make your code work, before going back to add and fix types afterwards. And with modern IDE’s or editor that supports TypeScript (e.g. VSCode, WebStorm and IntelliJ which has native support, while Atom and Sublime has support through plugins), you’ll even get live type-checking and auto completion right in your editor.
Caveat
Now there are also some minor disadvantages, mostly stemming from Babel not supporting cross-file information on emit:
- Legacy type casting (e.g.
const foo = <Foo>bar;
) is not supported. Useas
instead:const foo = bar as Foo;
. const enum Enum {}
is not supported, as it requires type information to compile. Use normal enums instead:enum Enum {}
.namespace
is not supported. Use ES6 modules (import
/export
) instead.- Legacy import/export syntax (
import =
/export =
) is not supported. Use ES6 styntax instead:export default
,export const
, andimport x, {y} from 'z'
Setup and configuration
Pre requirement:
If your project is not yet upgraded to use Babel 7, do a quick upgrade by following Babel’s own migration guide, or use their own upgrade cli tool (Simply run npx babel-upgrade --write
).
Setup babel/typescript:
Add @babel/preset-typescript + two plugins for full TypeScript support.
npm i --save-dev @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
Add the plugins to your Babel config (
.babelrc
orbabel.config.js
):
// babel.config.js
module.exports = {
presets: [
...,
'@babel/typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/proposal-object-rest-spread'
],
...
};
- (If you use webpack) Update resolution extensions and
babel-loader
rule test to read .ts(x) files.
// webpack.config.js
module.exports = {
...
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
},
module: {
rules: [
{
test: /\\.(js|ts)x?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
...
]
}
...
};
\t\t```
That’s it, Babel can now transform your TypeScript into JavaScript!
Now let`s move on to type-checking. We need the TypeScript Compiler for this. Let’s go ahead and see how we can best incorporate it.
**Configure TypeScript:**
1. Install the TypeScript Compiler
`npm i --save-dev typescript`
2. Install types for the frameworks and libraries you depend on (e.g. React, react-router-dom, redux etc.)
`npm i @type/react @types/react-dom`
3. Add `tsconfig.json` to your root directory: (for more information about tsconfig options see https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)
```javascript
// tsconfig.json
{
'compilerOptions': {
// Base directory to resolve non-relative module names (project root)
'baseUrl': '.',
// Target latest version of ECMAScript.
'target': 'esnext',
// Search under node_modules for non-relative imports.
'moduleResolution': 'node',
// Process & infer types from .js files.
'allowJs': true,
// Don't emit; only do type-checking, leave trasformation to Babel.
'noEmit': true,
// Enable strictest settings like strictNullChecks & noImplicitAny.
'strict': true,
// Disallow features that require cross-file information for emit.
// Cross-file information is not supported by Babel.
'isolatedModules': true,
// Import non-ES modules as default imports.
'esModuleInterop': true,
// Allow default imports from modules with no default export (e.g
// import React from 'React')
'allowSyntheticDefaultImports': true,
// Support jsx in .tsx files (https://www.typescriptlang.org/docs/handbook/jsx.html)
'jsx': 'preserve',
// Library files to be used in the project.
// Tells the compiler that 'DOM-APIs' and new ECMAScript features are valid.
'lib': ['dom', 'es2018'],
// Module aliases (if you use module aliases in webpack)
'paths': {
'yourModule': ['./some/modulefile.ts']
}
},
'include': ['src'],
'exclude': ['node_modules']
}
- Add new tasks to
package.json
// package.json
{
\t\t'scripts': {
\t\t\t'check-types': 'tsc',
\t\t\t'check-types:watch': 'tsc --watch'
\t\t}
}
Now you can run npm run check-types
to get your types checked. You’ll most likely want to add this to your build step, so that any type-check errors breaks your build. E.g 'build': 'npm run check-types && webpack --mode production …'
While developing, you can run npm run check-types:watch
to have a live type-checking as you code. As mentioned, if you’re using an IDE or editor that supports TypeScript , there is no need for the :watch
task, as your editor does this for you.
Extra
Jest
Jest will use Babel to transform your code whenever a Babel config is found in your root directory.
This makes it super easy to enable TypeScript with Jest. All you need is to specify is that .ts
and .tsx
files should also be read and transformed, as well as install types for Jest and any other library your using with it.
- In
jest.config.js
add the following properties:
//jest.config.js
module.exports = {
...
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRegex: '(/__tests__/.*|(\\\\.|/)(test|spec))\\\\.(js|ts)x?$',
transform: {
'^.+\\\\.(js|ts)x?$': 'babel-jest'
}
...
}
- Install types, e.g.
npm i -D @types/jest @types/enzyme @types/enzyme-adapter-react-16 @types/redux-mock-store
Now you can both import and write TypeScript in your test files.
Configuring ESLint to lint TypeScript files
Since our code base will be mixing *.js(x)
and *.ts(x)
files for awhile, I found that leveraging ESLint to do the linting of both to be the easiest. If your code base only contains *.ts(x)
files, then replacing ESLint with TSLint probably is better.
To enable linting of TypeScript in ESLint we added a plugin called eslint-plugin-typescript
. This plugin adds a set off TS linting rules to ESLint, many of which are from TSLint. To enable parsing of *.ts(x)
in ESLint, we use as separate parser, typescript-eslint-parser
. (In the upcoming 1.0.0 release of eslint-plugin-typescript
, the plugin will be providing it own version of the parser, so you don’t have to install it separately). We then added a override rule set to our .eslintrc
for *.ts(x)
files.
Let’t take a closer look at this:
Install
eslint-plugin-typescript
andtypescript-eslint-parser
:npm i -D eslint-plugin-typescript typescript-eslint-parser
Add override rule set to
.eslintrc
{
...
'overrides': [
{
'files': ['**/*.ts', '**/*.tsx'],
'excludedFiles': '**/*.js',
'parser': 'typescript-eslint-parser',
'parserOptions': {
'ecmaVersion': 2018,
'sourceType': 'module',
'ecmaFeatures': {
'jsx': true
}
},
'plugins': ['typescript'],
'rules': {
'typescript/rule-name': 'error'
}
}
],
...
}
Now you can see https://github.com/bradzacher/eslint-plugin-typescript#supported-rules for a full list of supported rules. Select the ones in line with your coding style.