Using subpath imports & path aliases
Subpath imports and TypeScript path aliases are useful and convenient features,
especially in large codebases. Both are pretty widely supported across runtimes
and bundlers for the web. However, the situation is different in more “vanilla”
setups when using the TypeScript compiler (tsc
) directly.
This article is about using subpath imports and path aliases with tsc
.
Specifically, we’re going to discuss two pitfalls when compiling to JavaScript
for a runtime like Node.js:
- Subpath imports are less well-known, and not fully supported in IDEs before TypeScript v5.4.
- TypeScript path aliases are not supported by Node.js.
tl/dr; See the recommendations and closing note at the end.
Subpath imports
Subpath imports are configured in package.json
, They’re a runtime and
dependency-free option to use aliases. Here’s an example import with a hash
specifier:
Internal subpath imports
are configured in package.json
like so:
Using *.js
in subpath imports configuration is essentially the same as
**/*.js
in glob patterns, so it recurses into subdirectories.
Make sure to check out the Node.js → subpath imports documentation for more features, such as conditional exports.
Problem
Support for subpath imports in package.json
has been in TypeScript since v4.5,
so tsc
compiles them just fine. But the TypeScript Language Server did not
fully catch up until v5.4.
Solution (option 1)
Upgrade TypeScript to v5.4.0+ and use only a single subpath "imports"
configuration in package.json
:
(Install typescript@beta
until latest
is 5.4.0
or higher.)
TypeScript will resolve paths properly and prioritize the aliases with auto-import suggestions in your IDE. Here’s an example of how that looks like:
Pros:
- Supported natively by Node.js (since v12.19.0/v14.6.0) and fully supported in TypeScript since v5.4.0.
- Subpath imports can make use of conditional exports.
Cons:
- Syntax is restricted to what subpath imports support:
- The
#hash
specifier syntax must be used (not@
or~
). - The
"#/*"
alias is invalid, but as short as e.g."#@/*"
is valid.
- The
If you have path aliases configured in tsconfig.json
you’d need to replace
them with subpath imports across your codebase.
If this is not an option for you, let’s discuss some alternatives.
TypeScript path aliases
Path aliases are a similar feature to subpath imports. Here’s an example configuration for the TypeScript compiler:
Problem
The TypeScript compiler (tsc
) does not rewrite import specifiers, so they’re
still the same when compiled to JavaScript:
However, this syntax is not supported during runtime in Node.js, resulting in an error:
$ node index.js
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '~' imported from [...]/index.js
Solution
We have some options here:
Option 2: Build time resolution
You can use path aliases and tsc-alias to convert them after the fact to
relative paths in the output that tsc
generates:
Pros:
- Use path aliases as supported by TypeScript.
- No duplicate configuration.
- No performance hit during runtime.
Cons:
- Requires a dependency (e.g.
tsc-alias
).
Option 3: Runtime resolution
Other solutions work at runtime. A popular option is tsconfig-paths.
After compilation with tsc
you can use a dependency like tsconfig-paths
as a
loader to convert the import paths during runtime:
Pros:
- Use path aliases as supported by TypeScript.
- No duplicate configuration.
Cons:
- Requires a dependency (e.g.
tsconfig-paths
). - Requires injection of a loader via command line or in code.
- Small(?) performance hit during runtime.
Recommendations
1. Relative paths
Your safest bet is to use no subpath imports or path aliases at all.
2. Subpath imports
Second best is to use only subpath imports (option 1), if supported by other tooling in your project such as TypeScript, test runners and code linters. The Node.js and Bun runtimes do support it.
3. Path aliases + build time resolution
And if that’s not an option yet, I’d recommend to use path aliases with build time resolution (option 2). This is fairly well supported across tooling today. There’s no runtime performance hit, and no risk of running the code in an environment that has no support.
Check out the documentation of your tooling to see what’s supported.
Closing Note
Subpath imports are perhaps less well known and less used today compared to TypeScript path aliases, but likely to become even more of a standard in the future. So subpath imports are generally recommended over path aliases going forward, especially considering support in TypeScript v5.4 has fully caught up.