loading...
Cover image for Restructure with ease thanks to Typescript path mappings

Restructure with ease thanks to Typescript path mappings

scooperdev profile image Stephen Cooper ・3 min read

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

Import Paths

Imports are a key part of any Typescript project, enabling 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';
Enter fullscreen mode Exit fullscreen mode

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 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 call paths which enables us to setup path mappings that we can use in our imports.

Given the following file structure we will setup 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
Enter fullscreen mode Exit fullscreen mode

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"],
    }
  }
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the first path mapping for app.

  "~app/*": ["app/*"]
Enter fullscreen mode Exit fullscreen mode

This is telling the Typescript compiler that whenever it sees an import starting with ~app/ that it should look for the code under the src/app/ folder. This enables us to update the import paths to both be '~app/common/sharedCode'. The trailing /* means 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';
Enter fullscreen mode Exit fullscreen mode

So despite having different relative paths, both modules now share exactly the same import path. You can hopefully 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 this time there is no * wildcard. Using this we can shorten the import path to this file.

  "~environments": ["environments/environment"]
Enter fullscreen mode Exit fullscreen mode
// 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';
Enter fullscreen mode Exit fullscreen mode

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 organise my imports. Without the ~ then 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';
Enter fullscreen mode Exit fullscreen mode

I really love using path mappings to clean up my import paths. They also mean that should I need to restructure my code, it will be a breeze. You can even do a simple find and replace now to update import paths should you wish to now.

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

Discussion

pic
Editor guide
Collapse
ramblingenzyme profile image
Satvik Sharma

At work we've just moved away from non relative imports to completely relative paths.

Few reasons for this:

  • Don't have to teach every tool we use how to resolve them
  • Don't have to teach everyone how we're special

This also leads to benefits like less configuration needed and that we can't break anyone's dev experience if their editor can't resolve an import, especially while we still have a mix of Flow/JS and are migrating to TS.

Collapse
luqeckr profile image
Luqman Hakim

you shouldn't change the baseUrl, it'll make your life easier..
the path will be like this:
"~app/*": ["src/app/*"],

with this, you don't have to change your imports all at once..

Collapse
scooperdev profile image
Stephen Cooper Author

Thanks for the heads up! I did not run into the issue of having to change all my imports at once with my current project but can see why this might cause issues.

Setup without modifying the value of the baseUrl.

"compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "~app/*": ["src/app/*"],
      "~environments": ["src/environments/environment"],
    }
  }
Collapse
esirei profile image
Esirei

Ah... I see you use ~ for your path prefixes. Personally I use @, but I've been thinking of changing it recently since I have a @types path that clashes with the @types libraries.
Thanks.

Collapse
richarddavenport profile image
Richard Davenport

I always prefix with the project prefix, or just @app.

Collapse
eecolor profile image
EECOLOR

For this reason we use leading slash imports. /* => src/*

Collapse
cname87 profile image
cname87

I had read the docs on these settings but didn't get the practical usage. Your article made that clear

Collapse
galgreenfield profile image
Gal

This makes me want to learn TypeScript! Thanks for the useful article. :)

Collapse
spock123 profile image
Lars Rye Jeppesen

You can do the same with babel

Collapse
scooperdev profile image
Collapse
cozydev profile image
CozyDev

It's posts like this that made me subscribe. Thanks for the tip!

Collapse
lucasdiedrich profile image
Lucas Diedrich

Thanks a lot, always wanted to learn about how could I get rid of import ../.

Collapse
rolandcsibrei profile image
Roland Csibrei

Cool! Thanks!

Collapse
lonlilokli profile image
Lonli-Lokli

Why do you care about it? It's automatically done by vs code.
Also you may get issues if you will try to convert your code into the library.

Collapse
scooperdev profile image
Stephen Cooper Author

While vs code can update your files, in my personal experience, it has not been 100% reliable in updating all my imports.

An additional benefit of the imports not changing with a refactor is that your pull requests will be much cleaner. In Github, for example, a file can then be reported as 'Moved with no file changes' which makes life a lot easier for your reviewer.

As for converting to library code I have actually seen that path mappings can be used positively. Say you have some shared code under your /app/common folder, you could setup a path mapping to it with the name @my-lib. This would enable you to refer to it like it was a real npm package. Then when you do extract the code into a separate package you just have to remove the path mapping and everything will just work with no updates to the rest of your code. Predicting the library name will be your biggest challenge here!

Would be interested to know what issues you are referring to in case I have missed something?

Collapse
mustafadal profile image
Mustafa Dal

What about IDE, for instance using by VSCode con you open the path with a sing click?

Collapse
scooperdev profile image
Stephen Cooper Author

Yes, I use vscode and this still works with the path mappings.

Collapse
mustafakunwa profile image
Mustafakunwa

It worked like a charm locally, but after uploading it doesn't work.
Can you help?

Collapse
scooperdev profile image