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';
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
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/ 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';
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"]
// 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 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';
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.
Stephen Cooper - Senior Developer at AG Grid
Follow me on Twitter @ScooperDev or Tweet about this post.
Top comments (19)
At work we've just moved away from non relative imports to completely relative paths.
Few reasons for this:
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.
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..
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.
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.
I always prefix with the project prefix, or just @app.
For this reason we use leading slash imports. /* => src/*
I had read the docs on these settings but didn't get the practical usage. Your article made that clear
This makes me want to learn TypeScript! Thanks for the useful article. :)
You can do the same with babel
Go for it! :)
It's posts like this that made me subscribe. Thanks for the tip!
Thanks a lot, always wanted to learn about how could I get rid of import ../.
Cool! Thanks!
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.
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?
What about IDE, for instance using by VSCode con you open the path with a sing click?
Yes, I use vscode and this still works with the path mappings.