Setting up Absolute Import Paths with Live Reloading (Typescript/Node)
Tags: typescript, node, tutorial, todayilearned
Published: 2020-07-01 13:19:51

## 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 -

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

But with a bit more grace, like this -

```typescript
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"

```sh
npm install -D typescript tsc-watch
```

*For Existing Typescript Project*

```sh
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

```json
{
    ...
    "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.

```json
{
    "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

```sh
npm run dev
```

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

For **Production**,

```sh
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).

```json
{
    "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`

```sh
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.

```json
{
    ...
    "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.

```typescript
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.**

```typescript
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.

```sh
npm install -S source-map-support
```

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

```typescript
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 -

```sh
npm install -D jest ts-jest @types/jest
```

```js
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] (https://bgxcode.com/posts/typescript/ts-absolute-import-paths/)