Restructure with ease thanks to Typescript path mappings

The hardest part about restructuring a TypeScript project is updating all of the import paths. While many of our favorite editors can help us, there is a powerful option hiding in the TypeScript compiler options that can remove this issue entirely.

Import paths

Imports are a key part of any TypeScript project as they enable us to include code from different files. A typical Angular component will have imports like this:

// Imports from node_modules
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
// Import from relative paths
import { UserComponent } from './user.component';
import { SharedAppCode } from '../../../common/sharedCode';

Importing from node_modules is always the same no matter where you are in your app. This is because, by default, any import not starting with a . or / is assumed to be found under node_modules.

For any shared code that lives in your app, depending on where your current file is located, you may have ../common, ../../common, ../../../common, or even ../../../../common.

These inconsistent relative paths are ugly and make restructuring our code painful. Move a file that contains these paths, and the imports will have to change accordingly. While this may seem like a small issue, it quickly gets out of hand in large apps. While code editors do their best to update the paths, sometimes they just stop working…

Fear not! TypeScript enables us to avoid this issue altogether.

tsconfig compilerOptions : { paths : { … } }

There is a compilerOption option called paths which enables us to set up path mappings that we can use in our imports.

Given the following file structure, we will set up two path mappings. One to enable us to import our sharedCode in each module without relative paths, and the second to import our environments file.

src/
├── app/
│   ├── common/
|   |   └── sharedCode.ts
│   ├── feature/
|   |   └── user/
|   |       └── user.module.ts
│   ├── feature2/
|   |   └── account.module.ts      
├── environments/
|   └── environments.ts
tsconfig.json

In our tsconfig.json file, we first need to set a baseUrl to tell the compiler where the paths are starting from. In our case, this is the src folder.

"compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "~app/*": ["app/*"],
      "~environments": ["environments/environment"],
    }
  }

Let’s take a look at the first path mapping for app:

  "~app/*": ["app/*"]

This is telling the TypeScript compiler that whenever it sees an import starting with ~app/, it should look for the code under the src/app/ folder. This enables us to update both the import paths to '~app/common/sharedCode'. The trailing /* means that we can include any folder path after that point. In our case,​ this is the common/sharedCode:

// BEFORE: user.module.ts
import {...} from '../../common/sharedCode';
// BEFORE: account.module.ts
import {...} from '../common/sharedCode';

// AFTER: user.module.ts, account.module.ts
import {...} from '~app/common/sharedCode';

So, despite having different relative paths, both modules now share exactly the same import path. Hopefully, you can see why this makes restructuring a lot easier. Now, if we change the folder structure, our imports no longer have to change.

You can also have explicit path mappings. Here, we are directly pointing at the environments file. Notice that, this time, there is no * wildcard. Using this, we can shorten the import path to this file:

  "~environments": ["environments/environment"]
// BEFORE: user.module.ts
import {...} from '../../../environments/environment';
// BEFORE: account.module.ts
import {...} from '../../environments/environment';

// AFTER: user.module.ts, account.module.ts
import {...} from '~environments';

Sensible import grouping

You may have noticed that I started both of my path mappings with a ~. The reason I have done this is so that I get a sensible grouping when I use Visual Studio Code to organize my imports. Without the ~, your app/common imports would be grouped with your external imports.

// EXTERNAL: From node_modules
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

// LOCAL: Path mapped 
import { SharedAppCode } from '~app/common/sharedCode';
// LOCAL: Relative path
import { UserComponent } from './user.component';

I really love using path mappings to clean up my import paths. Path mappings also mean that if I need to restructure my code, it will be a breeze. If you wanted to, you could even do a simple find and replace to update import paths.

Do let me know if you have any other ideas for making your code more refactorable!