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 thebaseUrl
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)