TypeScript with Babel: A Beautiful Marriage

Matt Turnbull
Aug 29, 2018 · 10 min read
Image for post
Image for post

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?

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.

Image for post
Image for post

Let me show you.

1) You already use Babel (or should).

  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.

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.

Image for post
Image for post
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.

But be careful. If the plugin alters the syntax significantly, then TypeScript may be unable to parse it. For example, the highly anticipated optional chaining proposal has a Babel plugin:

Image for post
Image for post
@babel/plugin-proposal-optional-chaining

But unfortunately TypeScript is unable to understand this updated syntax.

Don’t stress, there’s an alternative…

Babel Macros

Instead of adding plugins to your Babel config file, you install the macro as a dependency and import it within your code. The macro kicks in when Babel is compiling, and modifies the code however it likes.

Here’s an example. Using idx.macro to scratch our itch until optional chaining proposal arrives.

import idx from 'idx.macro';const friends = idx(
props,
_ => _.user.friends[0].friends
);

Compiles to:

const friends =
props.user == null ? props.user :
props.user.friends == null ? props.user.friends :
props.user.friends[0] == null ? props.user.friends[0] :
props.user.friends[0].friends

Macros are pretty new, but quickly gaining traction. Especially since landing in create-react-app v2.0. CSS in JS is covered: styled-jsx, styled-components, and emotion. Webpack plugins are being ported over: raw-loader, url-loader, and filesize-loader. And many more listed on awesome-babel-macros.

Here’s the best part: unlike Babel plugins, all Babel macros are compatible with TypeScript. They can also help reduce run-time dependencies, avoid client-side computation, and catch errors earlier at build-time. Check out this post for more details.

Image for post
Image for post
A better console.log: scope.macro

2) It’s easier to manage ONE compiler.

In the gloomy days (before Babel 7).

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.

Image for post
Image for post

The bright sunny days (with Babel 7).

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:

Image for post
Image for post
Babel has you covered.

3) It’s faster to compile.

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.

“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.

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) Const enums.

This is a shame. Need to resort to regular enums for now.

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!

Image for post
Image for post
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.

What about linting?

Update (Feb 2019): Use ESlint! The TypeScript team are focusing on ESLint integration since January. It’s easy to configure ESLint thanks to the @typesript-eslint project. For inspiration, check out my mega ESLint config which includes TypeScript, Airbnb, Prettier, and React.

Babel + TypeScript = Beautiful Marriage.

Image for post
Image for post
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.

Image for post
Image for post

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

The TypeScript team are working hard to spread the love. This Babel preset was a year long collaboration, and their new focus is on improving ESLint integration. This is a smart move — leverage the features, community, and plugins of existing tools. To develop competing compilers and linters is wasted effort.

The path to TypeScript is paved by simply tweaking the config of our favourite tools. The barrier to entry has been smashed.

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 now integrated into create-react-app v2.0, exposing 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.

Image for post
Image for post

It’s impossible to keep up with JavaScript.

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

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

DailyJS

JavaScript news and opinion.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store