This post might be helpful if you are looking to use path aliases within a javascript or typescript project.

What are path aliases?

Path aliases are the mapping of some kind of shorthand identifier to an import path (usually) within your project.

A relative import path like ../../data/types could use an alias like @shared/data/types.

In effect, whenever your tooling encounters the shorthand reference it will know where to look to actually import the desired module.

Why might you want to use them?

Improved readability

There is a cognitive load when importing relative modules in your code using explicitly relative paths. It will usually look something like this:

import { maxCacheAgeInMS } from "../../../config";
import { CompanySummary, DynamoDBQueryRecord, Product } from "../../data/types";
import chunkArray from "../../utils/chunk";

Whilst there is nothing wrong with this why not look at the alternative. What if, instead of a relative path to our shared folder being used every time we want to import an module from there we could use a fixed shorthand? Then it could look something like this:

import { maxCacheAgeInMS } from "@config";
import { CompanySummary, DynamoDBQueryRecord, Product } from "@shared/data/types";
import chunkArray from "@shared/utils/chunk";

In this example @shared is a path alias that has been setup to map to [projectRoot]/src/shared meaning that wherever we use it within our project our toolchain of choice will be able to find and import the same module.

I prefer the readability of this setup and find the cognitive load of reading it much smaller.

Encapsulation

When we use path aliases it can make it easier for us to refactor or change our project structure, if we wanted to move the location of our shared modules we can update the alias mapping in one place.

Whilst most modern text editors-cum-IDEs (VSCode included) can handle this for us by monitoring our project structures and updating imports for us when it changes it doesn’t always work as expected (especially in large projects/codebases) and often has some developer overhead of checking and confirming each change is valid.

Using path aliases within a project can help encapsulate this location logic within the codebase itself. On it’s own this is not the strongest argument for path aliases but builds on the benefits of increased readability.

What is the catch?

There is no such thing as a free lunch and whilst using path aliases comes with some benefits there is some setup and configuration required. There are some potential additional headaches in store for the tooling that is used around your project like:

  • Building/bundling, eg. babel, esbuild, tsc, webpack etc.
  • Testing, eg. jest etc.
  • IDE support, eg. VSCode, Webstorm etc.

This post focuses on the use of path aliases with:

  • VSCode
  • Jest
  • Webpack
  • Typescript

For the most part the configurations can be defined once and shared between these tools, this is great and reduces the potential headaches (and complexity) or setting up and maintaining path aliases in multiple places.

Configuring path aliases

Remember that path aliases are essentially a mapping between a shorthand reference and a qualified path. When configuring path aliases this is usually represented as a key -> value mapping that is observed by your tooling. The remaining questions are where does it go and what does it look like? We will walk through the answers to those now.

Typescript and IDE (our source of truth)

Typescript has the concept of projects and all your code must belong to a project, ordinarily this root of your repository represents the root of your project but not always. The Typescript compiler, tsc, looks for tsconfig.json files below the directory from which it has been run in or pointed at. Every directory in which it finds a tsconfig.json file is treated as the root of a separate Typescript project. For the purposes of this post we will assume you are working with a single Typescript project. Note that if you have multiple projects your aliases defined in a tsconfig.json are only valid within the scope of that project.

Whereas Typescript expects a tsconfig.json file to be present, pure Javascript projects don’t require an analogous jsconfig.json file. The concept of a jsconfig.json file was introduced by VSCode and modelled on the tsconfig.json spec used by Typescript. VSCode uses this file to, amongst other things, help Intellisense make sense of path aliases. Whilst it was introduced by VSCode it has been adopted or supported by other IDEs like Webstorm (since 2019.2).

The config

There is a paths property under compilerOptions in the tsconfig.json spec as you can see below. This is where our alias mappings should go:

{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@shared/*": ["src/shared/*"],
            "@config": ["config.ts"]
        }
    }
}

In this example you can see that you can point an alias to a specific file if you want to though usually a wildcard (*) is included that will be expanded to the remainder of the import path after the alias value. For example @shared/data/types would be expanded to src/shared/data/types at module resolution time.

There is also a baseUrl property under compilerOptions, if set the path aliases values are relative to this.

At this point the Typescript compiler and VSCode should quite happily be able to consume our path aliases within our project. VSCode may need a few minutes in a particularly big project, alternatively you can reload the project from the Command Palette.

Single source of truth

Our tsconfig.json or jsconfig.json will be used as the single source of truth for our path aliases within our project. How this is achieved for the rest of out toolchain varies from tool to tool but there are some handy helper modules we can use to make it relatively straight forward.

Jest

Unless otherwise specified Jest will assume your configuration is within your package.json file or a standalone jest.config.js file.

The Jest configuration property, moduleNameMapper, can be set and will inform Jest how to manipulate imports as required. This includes handling path aliases like those set up in the previous step. This property can look something like this:

{
  moduleNameMapper: {
    "^@shared/(.*)": "<rootDir>/src/shared/$1",
    "^@config$": "<rootDir>/config.ts",
  },
};

A glob/regex is used to match against import paths and mapped to one or more possible locations where these modules may be imported from.

I should note here that the moduleNameMapper configuration is not specifically for handling path aliases, it is intended for any use case where modifying imports in the test environment is desirable. For more information see the documentation.

Using our single source of truth

We are using our tsconfig.json or jsconfig.json files as a source of truth for our path aliases. With jest that are a couple of ways to do this.

If you are using ts-jest it comes bundled with a handy utility function that can be used to transform the alias config from your tsconfig.json into what Jest requires. Usage is as follows:

const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig");

{
 moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: "<rootDir>",
  })
}

As you can see passing a prefix of “<rootDir>” is often required as that is Jest’s representation of the root of your project and the mapper utility makes no assumptions about prefixes for your paths.

If you are not using ts-jest or do not want to couple yourself to ts-jest in this manner there is a standalone npm module jest-module-name-mapper that does the job as well. Usage is as follows:

const { default: tsconfigTransformer } = require("jest-module-name-mapper");

{
 moduleNameMapper: tsconfigTransformer("./tsconfig.json")
}

// OR (as per the readme)

{
 moduleNameMapper: require("jest-module-name-mapper")("./tsconfig.json")
}

As you can see the usage is pretty much similar. The main difference is that jest-module-name-mapper does make some sensible assumptions about the prefix jest requires for your paths and also about the location of your tsconfig.json file. I’ve passed in the location explicitly for clarity but this would have been the assumed location anyway.

Webpack

If you are using Webpack then it too will need to know how to handle your path aliases. There is a webpack configuration property resolve which has a child property alias (as seen below, docs), this is where we can let Webpack know how to handle our aliases. As with jest there is a handy npm module (or two) that can use your tsconfig.json as the source of truth for your aliases. These are:

I prefer the latter as the former replaces the context of resolve.aliases making it difficult to add aliases in addition to those within your tsconfig.json. Ultimately choosing which to use should be a decision based on the needs and context of your use case. Below is the usage of resolve-ts-aliases:

import { resolveTsAliases } from "resolve-ts-aliases";

const config: webpack.Configuration = {
  resolve: {
    alias: resolveTsAliases(path.resolve("tsconfig.json")),
  },
};

How you are using Webpack in your project will determine whether this alias mapping will be required, but if you find that Webpack cannot resolve your aliases the configuration above should hopefully do the trick.

Other options?

Most Javascript projects of a certain size, especially anything being shipped out to the world, will have some build tooling. The common build tools have been covered here but if you have a project with no tooling, for example you intend to run your script with node index.js then the npm module module-alias can be used. It intercepts node’s module resolution process to resolve any known aliases. Unfortunately, whilst it can play nice with webpack it struggles to do so with Jest (because Jest bypasses node’s module resolution) and may present challenges with other toolchains. How to use path aliases in this way is out of scope of this post but check it out if this sounds like it might be useful.

Demo repository

There is a repository with a tiny demo project with path aliases configured and working with Typescript, Jest and Webpack available here.

Get in contact

If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.