How to import modules using non-relative paths in TypeScript and how to transpile the result to JavaScript with correct path resolution.
When using relative paths in a TypeScript project of a certain size and complexity the imports start to look like this:
import { SpecialCollection } from "../../special";
import { LoginComponent } from "../login";
import { TextUtils } from ".../../utils/text";
import { Router } from "../../../core/router";
It gets cumbersome to import modules this way and it gets harder to refactor.
The solution presented in this article can be summarized in the following:
- Configure TypeScript to use non-relative paths.
- Use TypeScript to transpile the code to ES6.
- Configure Babel to transform the paths.
- Use Babel to transpile the code to ES5.
- Bundle the result for the browser using Browserify.
Setup custom paths in TypeScript
The TypeScript compiler can be configured with the following properties:
baseUrl
: the base path for your modules.paths
: path mappings relative tobaseUrl
.
You can read more about these and other settings in Module Resolution.
Let's consider the following project structure:
src
│ main.ts
│
└───core
│ │ router.ts
│
└───components
│ special.ts
│
└───auth
│ │ login.ts
│
└───utils
│ text.ts
tsconfig.json
could be configured like this:
{
"files": [
"src/main.ts"
],
"compilerOptions": {
"target": "es6",
"baseUrl": "./src",
"paths": {
"@core/*": ["core/*"],
"@components/*": ["components/*"],
"@auth/*": ["components/auth/*"],
"@utils/*": ["components/utils/*"]
}
}
}
Your imports could now be changed into the following:
import { SpecialCollection } from "@components/special";
import { LoginComponent } from "@auth/login";
import { TextUtils } from "@utils/text";
import { Router } from "@core/router";
The TypeScript compiler won't change the paths in the ouput. These options are there to inform the compiler where to find the modules, but the compiler will not transform the paths in the final ouput. If you run the transpiled code in the browser it won't know where to find the modules.
Setup custom paths in Babel
We told TypeScript to transpile to ES6. We will use Babel to transform the paths and transpile to ES5.
To resolve the modules in Babel we will use babel-plugin-module-resolver.
Our .babelrc
could look like this:
{
"presets": ["env"],
"plugins": [
["module-resolver", {
"root": ["./src"],
"alias": {
"@core": "./src/core",
"@components": "./src/components",
"@auth": "./src/components/auth",
"@utils": "./src/components/utils"
}
}]
]
}
root
and alias
are similar to baseUrl
and paths
from TypeScript but do notice the differences.
Build the final output
Now we can setup a gulp script do the compilation.
Create your project:
npm init
Install the necessary packages:
npm install --save-dev babel-plugin-module-resolver babel-preset-env babelify browserify gulp tsify typescript vinyl-source-stream watchify
An example gulpfile.js
:
var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var tsify = require("tsify");
var watchify = require("watchify");
var babelify = require("babelify");
var watchedBrowserify =
watchify(
browserify({
"basedir": ".",
"entries": "src/main.ts"
"debug": true,
"cache": {},
"packageCache": {}
})
.plugin(tsify)
.transform(babelify, { "extensions": [".js", ".ts"] })
);
function build() {
return watchedBrowserify
.bundle()
.on('error', function(error){
console.log(error.message);
this.emit('end');
})
.pipe(source("bundle.js"))
.pipe(gulp.dest("dist"));
}
gulp.task("build", [], build);
watchedBrowserify.on("update", build);
watchedBrowserify.on('log', function (msg) {
console.log(msg);
});
tsconfig.json
and .babelrc
will be read automatically by the plugins.
In summary, the TypeScript source files will be transpiled using tsify, then transformed using babelify where the final paths will be set and finally bundled for the browser with Browserify.