TypeScript with Babel: A Beautiful Marriage

TypeScript has never been easier thanks to the TypeScript plugin for Babel (@babel/preset-typescript), an official year-long collaboration between the TypeScript and Babel teams. Discover four reasons why TypeScript and Babel are a perfect pair, and follow a step-by-step guide to upgrade your project with TypeScript in 10 minutes.

Huh? What? Why?

I didn’t understand the need for this new preset at first.

Aren’t Babel and TypeScript two completely different things? How can Babel handle the TypeScript type checking? TypeScript can already output to ES5 just like Babel can, so what’s the point? Isn’t merging Babel and TypeScript complicating things?

After hours of research, my conclusion:
TypeScript and Babel are a beautiful marriage.

Let me show you.

1) You already use Babel (or should).

You’re in one of these three categories:

  1. You already use Babel. If not directly, then your Webpack config feeds *.js files into Babel (this is the case for most boilerplates, including create-react-app).
  2. You use Typescript without Babel. Consider adding Babel to your arsenal, it provides many unique features. Read on.
  3. You don’t use Babel? It’s time to jump on board.

Write modern JavaScript without breaking anything.

Your JavaScript code needs to run in an old browser? No problem, Babel converts the code and makes everything a-okay. Use the latest and greatest features without worry.

The TypeScript compiler has a similar feature, enabled by setting target to something like "ES5" or "ES6". But the Babel configuration improves on this with babel-preset-env. Instead of locking in a specific set of JavaScript features (ES5, ES6, etc), you list the environments you need to support:

"targets": {
"browsers": ["last 2 versions", "safari >= 7"],
"node": "6.10"
}

Babel uses compat-table to check which JavaScript features to convert and polyfill for those specific target environments.

Take a moment to appreciate the genius who named this project ‘compat-table’.

An interesting technique used by create-react-app: compile with the latest browsers during development (for speed), and compile with a larger range of browsers in production (for compatibility). Nice.

Babel is super configurable.

Want React with JSX? Flow? TypeScript? The shiny new optional chaining proposal? Just install a plugin and let Babel handle it.

@babel/plugin-proposal-optional-chaining

There’s also a great selection of third-party Babel plugins. Using Styled Components? Improve server side rendering. Using Ramda? Easily compose functions. Want more? Manage feature flags, improve lodash imports, enhance console.log, or strip console.log. Whatever floats your boat, you’ll find it on this mega list: awesome-babel.

Better than console.log: babel-plugin-console

Macros!

You know Kent C Dodds? He’s created a game-changing Babel plugin: babel-plugin-macros.

Instead of adding plugins to your Babel config file, you import macros directly in your JavaScript code. At build time, the macro kicks in and modifies the code. Check out this post for more details.

I think Babel macros will explode this year. Not only is it an amazing concept, but it’s already integrated into create-react-app v2.0 (beta), which pushes the community support.

2) It’s easier to manage ONE compiler.

TypeScript requires it’s own compiler — it’s what provides the amazing type checking superpowers.

In the gloomy days (before Babel 7).

Chaining together two separate compilers (TypeScript and Babel) is no easy feat. The compilation flow becomes: TS > TS Compiler > JS > Babel > JS (again).

Webpack is often used to solve this problem. Tweak your Webpack config to feed *.ts into TypeScript, and then feed the result into Babel. But which TypeScript loader do you use? Two popular choices are ts-loader and awesome-typescript-loader. The README.md for awesome-typescript-loader mentions it might be slower for some workloads, and recommends ts-loader with HappyPack or thread-loader. The README.md for ts-loader recommends combining with fork-ts-checker-webpack-plugin, HappyPack, thread-loader, and / or cache-loader.

Eugh. No. This is where most people get overwhelmed and put TypeScript in the “too hard” basket. I don’t blame them.

The bright sunny days (with Babel 7).

Wouldn’t it be nice to have one JavaScript compiler? Whether your code has ES2015 features, JSX, TypeScript, or something crazy custom — the compiler knows what to do.

I just described Babel. Cheeky.

By allowing Babel to act as the single compiler, there’s no need to manage, configure, or merge two compilers with some convoluted Webpack sorcery.

It also simplifies the entire JavaScript ecosystem. Instead of linters, test runners, build systems, and boilerplates supporting different compilers, they just need to support Babel. You then configure Babel to handle your specific needs. Say goodbye to ts-node, ts-jest, ts-karma, create-react-app-typescript, etc, and use the Babel support instead. Support for Babel is everywhere, checkout the Babel setup page:

Babel has you covered.

3) It’s faster to compile.

Warning! You might want to sit down for this bit.

How does Babel handle TypeScript code? It removes it.

Yep, it strips out all the TypeScript, turns it into “regular” JavaScript, and continues on its merry way.

This sounds ridiculous, but this approach has two strong advantages.

The first advantage: ️⚡️ IT’S LIGHTNING FAST ⚡️.

Most Typescript developers experience slow compilation times during development / watch mode. You’re coding away, you save a file, and… then… here it comes… annnnd… finally, you see your change. Oops, made a typo, fix that, save it, annnnd… eugh. It’s just slow enough to be annoying and break your momentum.

It’s hard to blame the TypeScript compiler, it’s doing a lot of work. It’s scanning for type definition files (*.d.ts), including within node_modules, and ensuring your code is used correctly. This is why many fork the Typescript type checking into a separate process. However the Babel + TypeScript combo still provides faster compilation thanks to Babel’s superior caching and single-file emit architecture.

So, if Babel strips out TypeScript code, what’s the point in writing TypeScript? That brings us to the second advantage…

4) Stay in the zone as you code.

You’re hacking some code together, quickly bashing out a solution to see if your idea has legs. You save the file, and TypeScript screams at you:

“No! I won’t compile this! Your code is broken in 42 different files!”

Yeah, you know it’s broken. You’ve probably broken a few unit tests too. But you’re just experimenting at this point. It’s infuriating to continuously ensure all your code is type safe all the time.

This is the second advantage of Babel stripping out TypeScript code during compilation. You write code, you save, and it compiles (very quickly) without checking for type safety. Keep experimenting with your solution until you’re ready to check the code for errors. This workflow keeps you in the zone as you’re coding.

So how do you check for type errors? Add a npm run check-types script that invokes the TypeScript compiler. I tweak my npm test command to first check types, and then continue running unit tests.

It’s not a perfect marriage.

According to the announcement post, there are four TypeScript features that do not compile in Babel due to its single-file emit architecture.

Don’t worry, it ain’t so bad. And TypeScript will warn against these issues when the isolatedModules flag is enabled.

1) Namespaces.

Solution: don’t use them! They’re outdated. Use the industry standard ES6 modules (import / export) instead. The recommended tslint rules ensure namespaces are not used.

2) Casting a type with the <newtype>x syntax.

Solution: Use x as newtype instead.

3) Enums that span multiple declarations (i.e. enum merging).

I’m not sure what this is. I have searched online for “enum merging” and found nothing. Can anyone help? Please let me know in the comments!

4) Legacy-style import / export syntax.

Examples: import foo = require(...) and export = foo.

In all my years of TypeScriptin’ I’ve never come across this. Who codes this way? Stop it!

Ok, I’m ready to try TypeScript with Babel!

Photo by rawpixel.com

Let’s do this! It should only take about 10 minutes.

I’m assuming you have Babel 7 setup. If not, see the Babel Migration Guide.

1) Rename .js files to .ts

Assuming your files are in /src:

find src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;

2) Add TypeScript to Babel

A few dependencies:

npm install --save-dev @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

In your Babel config file (.babelrc or babel.config.js):

{
"presets": [
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}

TypeScript has a couple of extra features which Babel needs to know about (via those two plugins listed above).

Babel looks for .js files by default, and sadly this is not configurable within the Babel config file.

If you use Babel CLI, add --extensions '.ts'

If you use Webpack, add 'ts' to resolve.extensions array.

3) Add ‘check-types’ command

In package.json:

"scripts": {
"check-types": "tsc"
}

This command simply invokes the TypeScript compiler (tsc).

Where does tsc come from? We need to install TypeScript:

npm install --save-dev typescript

To configure TypeScript (and tsc), we need a tsconfig.json file in the root directory:

{
"compilerOptions": {
// 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; allow Babel to transform files.
"noEmit": true,
// Enable strictest settings like strictNullChecks & noImplicitAny.
"strict": true,
// Disallow features that require cross-file information for emit.
"isolatedModules": true,
// Import non-ES modules as default imports.
"esModuleInterop": true
},
"include": [
"src"
]
}

Done.

Well, the setup is done. Now run npm run check-types (watch mode: npm run check-types -- --watch) and ensure TypeScript is happy with your code. Chances are you’ll find a few bugs you didn’t know existed. This is a good thing! The Migrating from Javascript guide will help here.

Microsoft’s TypeScript-Babel-Starter guide contains additional setup instructions, including installing Babel from scratch, generating type definition (d.ts) files, and using it with React.

I’m using create-react-app.

It’s a little fiddly, but still possible! First, upgrade to create-react-app v2.0 (beta). Then setup react-app-rewired, which lets you override parts of the configuration. And finally, install this rewired plugin: react-app-rewire-typescript-babel-preset.

What about linting?

Use tslint.

npm install --save-dev tslint

Create a tslint.json file in the root directory:

{
"defaultSeverity": "error",
"extends": [
"tslint:latest",
],
"jsRules": {},
"rules": {},
"rulesDirectory": [],
"linterOptions": {
"exclude": ["**/node_modules/**"]
}
}

Setup a lint command in package.json file:

"scripts": {
"lint": "tslint '**/*.ts'"
}

That’s it! Now call npm run lint to lint your project. If you’re using VS Code, install the vscode-lint extension for red squiggly lines.

If you use Prettier to automatically format your code, keep using it! It has built-in support for TypeScript files. Integrate with linting using tslint-config-prettier.

If you use React, you might enjoy some React rules provided by tslint-react.

Alternatively, you can keep eslint and use typescript-eslint-parser with eslint-plugin-typescript to add TypeScript rules. I haven’t tried this combination — please let me know in the comments if this works better than tslint.

There’s also the Google TypeScript style project which combines zero config linting and formatting (like Prettier, but uses clang-format). I haven’t tried this either, and would love to know your thoughts in the comments.

Babel + TypeScript = Beautiful Marriage.

Photo by Akshar Dave

Babel is the one-and-only JavaScript compiler you need. It can be configured to handle anything.

There’s no need to battle with two competing JavaScript compilers. Simplify your project configuration and take advantage of Babel’s amazing integration with linters, test runners, build systems, and boilerplates.

The Babel and TypeScript combo is lightning fast to compile, and allows you to stay in the zone as you code, and check types only when you’re ready.

Prediction: TypeScript will rise.

According to the most recent Stack Overflow Developer Survey, JavaScript is the most popular language, with TypeScript trailing at #12. This is still a great achievement for TypeScript, beating out Ruby, Swift, and Go.

I predict TypeScript will crack the top 10 by next year.

You can start with vanilla JavaScript and easily upgrade to TypeScript if you feel the need. The barrier to entry has been smashed thanks to Babel.

The TypeScript team are working hard to spread the love. This Babel preset was a year long collaboration, and they’ve contributed heavily to typescript-eslint-parser, which powers Prettier’s TypeScript support.

With the rise in popularity of VS Code, developers are already setup with an amazing TypeScript environment. Autocomplete on steroids will bring tears of joy.

It’s also likely to be integrated into create-react-app itself, thanks to support from Jared Palmer (creator of Formik, Razzle, and Backpack). There’s a pending pull request, and if accepted will expose TypeScript to an audience of 200k downloads per month.

If you’ve been put off by TypeScript because it’s difficult to setup, it’s no longer an excuse. It’s time to give it a go.

It’s impossible to keep up with JavaScript.

You’re catching up every chance you get. Scrolling… reading… refreshing… skimming. You’re lost in 42 browser tabs of articles, tutorials, and GitHub repos. You bookmark a handful to check out later (or, never).

It’s overwhelming. There’s too much to know.

So I developed a system that easily keeps me up-to-date.