TypeScript, Mocha and Babel

Petr Gazarov
policygenius-engineering
6 min readAug 4, 2017

--

I’ve recently set up TypeScript in an existing codebase that uses Webpack, Mocha and several Babel plugins, including babel-plugin-rewire. I’m inspired by Randy Coulman’s super detailed post on mocha-webpack, so I decided to share the results of my journey.

Part 1 — Overview of libraries

⚠️ If you just want a working setup, feel free to skip to Part 2.

NB: All typescript tools below use tsconfig.json file for configuration.

awesome-typescript-loader

A TypeScript loader for webpack. There is a good chance that all you need is a TypeScript loader.

You can control the output with module and target options, and configure baseUrl and paths for dynamic path resolution. Compiler configuration has many useful options, both for transpilation and type checking. If you are not using Babel for anything other than basic JS transpilation, you can probably stop here and just replace Babel loader with it.

If you are using both TypeScript loader and Babel, awesome-typescript-loader has option useBabel. Can be useful when you want to control the transpilation all in one config.

babel-loader

A Babel loader for webpack. Babel is an extensible and very common compiler for JavaScript. From docs:

Out of the box Babel doesn’t do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again.

Its power lies in plugins that you can add to customize. You can also write your own plugin.

A preset is a combination of plugins. With Babel, it is possible to use presets for some of the experimental JavaScript features that have not been confirmed in the next release yet.

Babel configuration is set in .babelrc or package.json. It supports splitting configuration into different environments, so you can easily require a plugin only for test environment.

mocha-webpack

This is a new-ish library that uses Webpack compilation and feeds it to Mocha.

Why? The traditional Mocha setup uses --require and --compiler options to transpile code. This adds a separate compilation config for test environment that needs to be kept up to date. mocha-webpack uses your existing Webpack config, and the code is compiled the same way almost entirely across both environments.

Because it uses Webpack, it works with awesome-typescript-loader, babel-loader and whatever other loaders you set up. From personal experience, once webpack config gets bigger, it can be challenging to replicate it with traditional mocha setup because there are often different tools for Mocha then Webpack and sometimes they don’t work the same way.

babel-plugin-rewire

A Babel plugin for a common testing library Rewire.js. You can use it when you want to mock variables or imports, and it is useful for unit tests. Check out the readme for code examples.

typescript tsc

Typescript comes with a built-in compiler. typescript npm package will give you tsc command which can be used to compile TypeScript or JavaScript files. tsc writes compiled files to disk, so it’s not very useful if you are already using Webpack.

It’s worth noting that TypeScript compiler has support for latest JavaScript features as well as jsx. Most existing TypeScript tools are using TypeScript native compiler under the hood.

TypeScript compiler is not as extensible as Babel. TypeScript just recently added support for custom transforms, and there are no short-term plans for adding plugin system similar to that of Babel. If you bump into this issue, you can use Babel and TypeScript compilers together.

ts-node

ts-node is useful for executing TypeScript files with node. It also has a REPL environment, similar to node. The github readme gives examples on how you can use together with Mocha, Tape and Gulp.

$ ts-node
> let multiply: (a: number, b: number) => number = (a, b) => a * b;
{}
> multiply(2,2)
4
> multiply(2, 'dog')
⨯ Unable to compile TypeScript
[eval].ts (1,12): Argument of type '"dog"' is not assignable to parameter of type 'number'. (2345)

For Mocha setup without Webpack, ts-node is what you need. You can pass it to mocha in a require hook and it will transpile files on the fly.

mocha --require ts-node/register [...args]

tsconfig-paths

ts-node does not resolve paths that rely on paths or baseUrl (see this issue). Instead, if you need those options, thetsconfig-paths plugin solves this problem.

mocha --require tsconfig-paths --require ts-node [...args]

babel-register

A way to use Babel through a require hook. It can be used through CLI or in JavaScript code directly.

When used through CLI, it is not possible to apply Babel to TypeScript files. This is because default extensions for babel-register are .es6, .es, .jsx and .js and extensions aren’t customizable with CLI use (extensions are not managed in .babelrc file).

If you need to use both TypeScript and Babel compilers in test environment, stick around for Part 2.

Part 2 — TypeScript, Webpack, Babel and Mocha setup

The motivation behind this setup is to be able to use both TypeScript and Babel compilers in test environment with Mocha. If you are using some Babel plugins with Mocha, and would like to keep them, this setup works (in my example, it is babel-plugin-rewire but can be good for any plugin that you don’t want to lose when you transition to TypeScript).

Additionally, this setup can feel more friendly to those familiar with Webpack and is a way to manage development and test compilation config in one place.

So, let’s get started!

We will need the following tools:

  • Webpack
  • Mocha
  • webpack-mocha
  • awesome-typescript-loader
  • babel-loader

Here I will assume you know what these tools are (read Part 1 if you need a refresher).

// webpack.config.jsconst config = {  // ...all of your other settings stay the same  module: {
loaders: [
{
test: /\.(ts|tsx)$/,
loader: 'awesome-typescript-loader',
exclude: /node_modules/,
options: { useBabel: true }
},
{
test: /\.(js|jsx)$/,
loaders: ['babel-loader'],
exclude: /node_modules/
}
// ...add any other loaders here
]
}
}
module.exports = config;

⚠️ Notice useBabel option. It tells TypeScript loader to delegate JS transpilation to Babel. It is usually not necessary, but we will need it for our plugin babel-plugin-rewire in a minute.

Then create a new webpack config file for test environment. This file will import your main webpack config, and will do a minor adjustment.

// webpack.test.config.jsconst config = require('./webpack.config.js');config.target = 'node';module.exports = config;

Here we changed target and exported the rest of the config. target is node for Mocha because the compiled JavaScript is run in node, as opposed to the browser.

// .tsconfig.json{
"compilerOptions": {
"moduleResolution": "node",
"module": "ES6",
"jsx": "React",
"target": "ES6"
}
}

Since we are delegating JS transpilation to Babel, most settings in this file are not used and .babelrc is used instead.

Your test command would look like this:

NODE_ENV=test ./node_modules/mocha-webpack/bin/mocha-webpack --webpack-config ./path/to/webpack.test.config.js [...args]

⚠️ Notice you don’t need to pass mocha options for the compilation. mocha-webpack passes already compiled files to mocha. If you have some extra options, you can use --opts arguments to pass them.

As a side benefit, mocha-webpack comes with a better watch mode. Instead of re-running all of your tests on file change, it only reruns tests that could be affected by the changed file.

A simplified babel configuration:

// package.json  "babel": {
"presets": ["latest", "react"],
"env": {
"test": {
"plugins": [
"babel-plugin-rewire"
]
}
}
}

babel-plugin-rewire needs original imports in order to work properly. TypeScript loader by default renames imports (and rewrites paths). useBabel option changes this behavior, and it passes down unchanged imports, which is what babel-plugin-rewire relies upon (see this issue if interested to learn more).

⚠️ Note that with useBabel set to true, you will need appropriate Babel plugins to handle path resolution.

If you are excited about TypeScript, I encourage you to share your own experiences to help adoption of the technology. Makes a big difference!

--

--