TypeScript, Mocha and Babel
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!