How to Integrate TypeScript with Babel

Updated Jul 12, 2023#typescript#babel#how-to

Babel (42.4 ⭐) and TypeScript (92.6k ⭐) are two essential tools for JavaScript developers, can be used as standalone tools, or combined with each other to take advantages of both.

Babel is the dominant JavaScript transpiler with hundreds of plugins to transform or polyfill your JavaScript code in whatever ways you want before running in a host environment.

TypeScript allows you to optionally add static type definitions on top of JavaScript, can be considered as a programming language with its own compiler or as a static type checker like Flow.

They have some overlap, but can certainly be used together. You can use Babel as a TypeScript compiler (tsc). This means much faster compilations, and you can use Babel plugins in TypeScript just as you would with JavaScript.

Before the inception of plugin @babel/preset-typescript, the best build pipeline is still to pass the TypeScript files to the TypeScript compiler and then to Babel afterwards.

Motivation

There is no need for Babel unless you got to do some case specific transformations.

Babel 7 is useful because it has really robust transforms and polyfills for older browsers. For example example, it will automatically add polyfills for things like promises, or new object methods, as you use them. This has been important for working with enterprise apps.

  • Babel has more fine-tuned config using compat-table to check which JavaScript features to convert and polyfill for those specific target environments.
  • Babel is much more extensible than TypeScript. There’s plenty of plugins that optimize your code, helps you strip out unused imports, inlines, constants and much more.
  • Babel play well with JavaScript ecosystem (bundlers, build systems, frameworks, test frameworks, linters)
  • Typescript has as little runtime impact as possible. Except for a very limited number of utility functions it has no other runtime behavior. This is by design. TypeScript doesn’t load polyfills automatically for you like Babel does.

While Babel can take over transpiling, it doesn’t have type-checking built in, and still requires using TypeScript to accomplish that. So even if Babel builds successfully, you might need to check in with TypeScript to catch type errors.

Combining TypeScript with Babel allows you to check for type errors only when you’re ready. This combo also provides faster compilation thanks to Babel’s superior caching and single-file emit architecture.

You have tsc compile to ES20-whatever, and then have Babel use preset-env to compile that down to whatever the least-qualified browser supports, pulling in polyfills along the way.

You will still need an editor with TypeScript support to benefit from type checking, but doing so will not affect the way their code is built and run.

Caveats

All sorts of caveat come from the fact that Babel doesn’t support cross-file analysis. TypeScript by default compiles an entire project at once, while Babel only compiles one file at a time. This means that Babel doesn’t support TypeScript features that require reading multiple files.

Babel doesn’t care about your fancy TypeScript types. It just throws them in the trash, without able to check that they’re right.

TypeScript namespaces and const enums don’t work with Babel. Don’t worry! These caveats aren’t too bad. You don’t miss too much here.

Be aware that Babel plugin mimics TypeScript compiler options and doesn’t read tsconfig.json, it means you must provide TypeScript configs twice, some compiler options might be handled differently.

Integration

As recommended by TypeScript team, transforming your files to JavaScript works via Babel, TypeScript compiler only for type checking and .d.ts file generation. This is a common pattern for projects with existing build infrastructure which may have been ported from a JavaScript codebase to TypeScript.

  1. Install packages
npm install --save-dev \
  @babel/core \
  @babel/cli \
  @babel/preset-env \
  @babel/preset-typescript \
  typescript
  1. Config .babelrc.json
{
  "presets": [
    "@babel/preset-typescript",
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": "> 0.25%, not dead"
      }
    ]
  ]
}
  1. Config tsconfig.json
{
  "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,
    // Import non-ES modules as default imports.
    "esModuleInterop": true,
    // Ensure that .d.ts files are created by tsc, but not .js files
    "declaration": true,
    "emitDeclarationOnly": true,
    // Ensure that Babel can safely transpile files in the TypeScript project
    "isolatedModules": true
  },
  "include": [
    "src" // <-- change this to where your source files are
  ]
}
  1. Config package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "npm run type-check -- --watch",
    "build": "npm run build:types && npm run build:js",
    "build:types": "tsc --emitDeclarationOnly",
    "build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline"
  }
}