DEV Community

Binura Gunasekara
Binura Gunasekara

Posted on • Edited on • Originally published at bgxcode.com

Setting up Absolute Import Paths with Live Reloading (Typescript/Node)

Quick Intro - What are Absolute Imports?

If you're new to the world of Typescript and haven't come across this before, it's basically a way to do this -

import { SomeModule } from '../../../server/services/some-module';

But with a bit more grace, like this -

import { SomeModule } from '@server/services/some-module';

This looks simple enough, why do I need this article?

You're right and it should be pretty straightforward, but unfortunately (as with many things in Typescript), it's not.
I've been working with Node and Typescript for a long time and I still have to pull my hair out every time I set up a new project - especially when it comes to
setting up tests and live-reloading.

Note

This tutorial is targeted specifically for Typescript with Nodejs. This will not work on other runtimes or front-end frameworks like React
(those will require a different setup).

1. Setting up Live-Reloading with TSC and Node

There are alternate libraries to help with transpiling Typescript and Live Reloading ('ie. watch for changes and recompile') such as TS-Node or TS-Node-Dev (TSND). These compile your Typescript to Javascript in memory and run it (..or something like that).

While I haven't found any specific benchmarks comparing TS-Node to native-Node performance, the general community consensus
is to run the typescript compiler (tsc) and run the generated Javascript with Node (as it was meant to be), which is unarguably the most
efficient way to run both in terms of resource usage and performance. We'll be taking this approach.

Install the required NPM packages

*For a New Project"

npm install -D typescript tsc-watch

For Existing Typescript Project

npm install -D tsc-watch

If you already have nodemon, ts-node or ts-node-dev installed, you can go ahead
and remove them as they will no longer be required.

Edit your package.json scripts

{
    ...
    "scripts": {
        "dev": "tsc-watch --onSuccess \"node dist/main.js\"",
        "build": "tsc",
        "start": "node dist/main.js"
    },
    ...
}

tsc-watch is a lightweight library that allows us to run a command after tsc --watch runs on a file change.
While you can achieve the same effect by using nodemon, this native approach works just as well, if not better. (It'll make your
life much easier if you're building a third-party library or looking at Incremental Typescript Compilation in the future).

My tsconfig puts the transpiled JS to a dist/ directory. Replace this with wherever your transpiled entry (main/index.js) file is.

Here's the tsconfig.json for reference.

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es2018",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "noImplicitAny": true,
        "esModuleInterop": true,
        "removeComments": true,
        "sourceMap": true,
        "baseUrl": ".",
        "outDir": "dist"
    }
}

And now we have Live-Reloading set up! Just run

npm run dev

and you're application will be recompiled and re-run every time you save a file.

For Production,

npm run build
npm start

2. Setting up Absolute Path Imports

Now we get down to business.

To enable absolute path imports with our live-reloading/production-build setup we need to let both the Typescript Compiler
and the Node runtime know where to look for the absolute imports.

For this tutorial, we'll create two folders, server and common.

2.1. Add paths to the tsconfig.json

Adding the paths property to the tsconfig.json lets the Typescript Compiler know where to look for the files
in our absolute import paths. BUT this does not mean that it will automatically resolve the path names when it compiles.
To do that, we'll install module-alias in the next step.

Important! The paths are relative to the baseUrl in your tsconfig.

Why are the paths prefixed with @?

I personally use this convention to avoid any potential confusion. It makes it clear that it's not an import from your node_modules
and that it's not a normal relative import. You can of course, leave out the @ or use a different symbol entirely. (~ is commonly used as well).

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es2018",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "noImplicitAny": true,
        "esModuleInterop": true,
        "removeComments": true,
        "sourceMap": true,
        "baseUrl": ".",
        "outDir": "dist",
        "paths": {
            "@server/*": ["src/server/*"],
            "@common/*": ["src/common/*"]
        }
    }
}

2.2. Adding Module-Alias to the package.json

npm install -S module-alias

In your package.json add the following property.

Remember 👈🏼

The paths in the tsconfig.json point to your source directory with the Typescript files.
This section however, must point to the folders that contain the respective transpiled Javascript files.

As the Typescript compiler does not resolve the paths during compilation, this will let the Node runtime
know where to look for your imports.

{
    ...
    "scripts": {
        "dev": "tsc-watch --onSuccess \"node dist/main.js\"",
        "build": "tsc",
        "start": "node dist/main.js"
    },
    "_moduleAliases": {
        "@server": "dist/server",
        "@common": "dist/common"
    },
    ...
}

2.3. Register Module-Alias in your Entry file

Now the only thing left is to make sure you add the following import to the top of your main/entry Typescript file.

import 'module-alias/register';

And that's it! You have succesfully configured Absolute Path imports with Live-Reloading in your Node/Typescript Project. 🍻

You can now import the modules in server/ and common/ from anywhere in your codebase.

import { User } from '@common/user';
import { Post } from '@common/post';
import Server from '@server/server';

3. Add Source Map support

I suggest you also add the source-map-support package to get better stacktraces that are linked back to your source Typescript files.
This will definetely make your life easier during development.

npm install -S source-map-support

And then register at the top of your entry file, just like we did with module-alias.

import 'module-alias/register';
import 'source-map-support/register';

And you're all set! 🎉

Example Code (Github)

You can find a bare-bones example for this tutorial here - https://github.com/binura-g/ts-absolute-import-paths-tutorial

If you run into any issues with this tutorial refer to this repository - the chances are you'll be able to figure out
what went wrong.

Extra: Writing Tests with absolute imports

To use Jest as your test runner (which I would also recommend as Jest + SuperTest is a really nice combo), edit your
jest.config.js as below -

npm install -D jest ts-jest @types/jest
module.exports = {
    rootDir: '.', // This should point to the rootDir set in your tsconfig.json
    globals: {
        'ts-jest': {
            tsConfig: '// the path to your tsconfig.json',
        },
    },
    verbose: true,
    preset: 'ts-jest',
    testEnvironment: 'node',
    moduleNameMapper: {
        '@server/(.*)': '<rootDir>/src/server/$1',
        '@common/(.*)': '<rootDir>/src/common/$1',
    },
};

Note that these paths under moduleNameMapper should point to your source Typescript files (similar to the paths in your tsconfig.json).
ts-jest will take care of transpiling your Typescript files as required during test runtime.

There are possibly numerous other ways to configure other test runners (like Mocha) to work with absolute imports but I unfortunately can't cover
every possible setup in this article. But if look around Google/Stackoverflow you'll most definitely find a way.

It's not easy to set this all up the first time, but it's definetely worth the effort. 😉

Happy Coding!

  • You can read this article on my DevBlog here

Top comments (0)