At Artsy we <3 TypeScript. We use it with React Native via Emission and on the web via Reaction. Until recently, however, projects that required the use of Babel had to implement convoluted tooling pipelines in order to work with the TypeScript compiler, increasing friction in an already complex landscape. (An example of this is Emission's use of Relay, which requires babel-plugin-relay to convert
graphql literals into require calls.) Thankfully, those days are over. Read on for an example project, as well as some advice on how to avoid common pitfalls when working with the new beta version of Babel 7.
Babel configurations can be complicated. They take time to set up and maintain and can often contain some pretty far-out features that make interop with other environments difficult. That's why we were elated when this PR appeared in the wild from @andy-ms, a developer on the TypeScript team, announcing a new parser for Babylon. @babel/preset-typescript arrived soon after and we felt it was finally time to give it a try. There was a catch, however: TypeScript support only works with Babel 7+!
Here's list of setup issues we faced in no specific order:
1) New @babel Namespace
One of the first things Babel 7 users will notice is the package ecosystem now exists as a monorepo and all NPM modules are namespaced behind the
@babel org address. Packages that used to be installed via
1 2 3 4 5
are now installed via
1 2 3 4 5
which immediately creates upgrade conflicts between libraries that use Babel 6 and Babel 7. For example,
babel-jest internally points to
babel-core which supports a version range between 6 and 7 -- but! --
babel-core is now
@babel/core so this breaks.
This wasn't immediately apparent at the time, and so we would often find errors like
These errors appeared ambiguous because the folder structure was correct and commands like
yarn list @babel/preset-env yielded expected results:
Why was the package not found? Digging deeper, it seemed like Babel 6 was still being used somewhere. Running
yarn list babel-core revealed the culprit:
Thankfully, babel-bridge exists to "bridge" the gap, but one can see how complications can and will arise. Further, not all packages have implemented this fix and so we had to rely on
yarn's new selective dependency resolution feature which overrides child dependency versions with a fixed number set directly in
1 2 3
With this in place many of our errors disappeared and packages like
jest now worked like a charm.
2) Missing ES2015 Features
Another error we faced early on surrounded language features that worked with Babel or TypeScript, but not with Babel and TypeScript. For example, take an existing Babel project that points to
index.js as an entrypoint, configure it to support TypeScript via Babel 7, and then run it:
1 2 3
1 2 3 4 5
1 2 3 4 5
Everything seems to be working; our
.js entrypoint is configured to support
.ts extensions and we kick off the boot process.
Let's now try to import a file from within
1 2 3 4 5 6 7 8
tsconfig.json file is misconfigured?
1 2 3 4 5
Nope, all good. How about my
1 2 3 4 5 6 7 8 9 10 11 12
@babel/preset-env which handles selecting the JS features we need, so thats not it. And anyways, doesn't TypeScript support
ES2015 modules right out of the box?
Continuing, how about specifying the extension list directly in
Still no go 🙁
Last try: Create a new entrypoint file that uses a
.ts extension and then use that to boot the rest of the app:
1 2 3 4
Once this change was in place, we could ditch
@babel/register and instead rely on the
--extensions configuration from
package.json, just like the README suggests (doh! 🤦).
Lastly, since Babel 7 is now responsible for compiling our TypeScript files we no longer need to rely on TypeScript's own
This reads in settings located in
1 2 3 4 5 6 7
noEmit flag? That tells
tsc not to output any JS and instead only check for correctness. The "pretty" flag gives us nicer type-checker output.
While this seemed to be all that was needed, running
yarn type-check would throw an error:
1 2 3 4 5 6 7 8 9 10
Why is it TypeChecking my
node_modules folder when
rootDirs is set to
src? It looks like we missed a TypeScript setting:
1 2 3 4 5
With that last missing piece everything now works:
1 2 3 4 5 6 7 8 9 10 11 12 13
Proper type-checking, but compilation handled by Babel 😎.
4) TypeScript and Flow
Unfortunately, the TypeScript and Flow plugins for Babel cannot be loaded at the same time, as there could be ambiguity about how to parse some code.
This is usually ok, because the general advice is to compile your library code to vanilla JS before publishing (and thus strip type annotations), but there are packages that could still enable the Flow plugin.
For example, the React Babel preset in the past would enable the Flow plugin without really needing it for its own source, but just as a default for consumers of React.
This issue cannot really be worked around without patching the code that loads the plugin. Ideally this patch would be sent upstream so that the issue goes away for everybody.
This issue can be worked around by either eliminating the dependency on the preset that loads the plugin, for instance by depending on the individual plugins directly, or if that’s not possible by patching the code. Ideally that patch should go upstream, of course, but if you need something immediate then we highly recommend patch-package, as can be seen used in this example.
There’s even projects that publish their Flow annotated code without compiling/stripping type annotations, the one we know of and use is React Native. There’s no way around this other than patching the code. You may think that you could use a plugin like babel-plugin-transform-flow-strip-types, but in reality that transform needs the Flow plugin to be able to do its work and thus is a no-go.
The way we’ve worked around that is by stripping Flow type annotations from all dependencies at dependency install time using the
flow-remove-types tool. It can get a little slow on many files which is why we do a bunch of filtering to only process files that have
@flow directives, the downside is that some files don’t have directives like they should and so we patch those to add them using the aforementioned patch-package.
5) Limitations in TypeScript support
It is important to note that you may run into a few cases that TypeScript’s Babel plugin does/can not support. From the plugin’s README:
Does not support
const enums because those require type information to transpile. Also does not support
import =, because those cannot be transpiled to ES.next.
The lack of namespace support hasn’t been a problem for us, we’re only using it in one place which could easily be changed to use regular ES6 modules as namespace. This is also why for instance the ‘recommended’ list of TSLint checks includes the
const enum feature is a runtime optimization that will cause the compiler to inline code. We don’t have a need for this at the moment, but some discussion is happening to possibly still being able to make use of this feature when compiling production builds with the TypeScript compiler instead.
export = and
import = syntax is meant to work with CommonJS and AMD modules; however, we strictly use ES6 modules.