How to have absolute import paths in a library project?

The paths mapping you establish in your tsconfig.json is purely a compile-time mapping. It has no effect on the code generated by the TypeScript compiler. Which is why you have a failure at run time. That’s something that has been reported to the TypeScript project, suggesting that tsc should automatically translate module paths in emitted code to conform to the mapping established by paths. The TS devs responded tsc is working as intended and that the solution is to configure a module loader that performs at run time a mapping similar to that established by paths.


Here what I think you should do, based on how you described your case.

I’m assuming that midi-app is a test application that is not meant to be distributed. You should be able to continue using the paths mapping you have without any issue. (You’ve not mentioned any issue running this app. So it seems your tooling already takes care of the runtime issue.)

For midi-lib, I would stop relying on the mappings established by paths and just use relative paths. This is a library, meant to be consumed by others. Because of this, any configuration that would fix the module name mapping at run time (or at bundling time) would have to be handled by the consumers of your library. Consumers that use Webpack will have to add a configuration to their Webpack configuration to provide the right mapping. Consumers that use Rollup would have to do the same with Rollup. Consumers that use SystemJS would have to do the same with SystemJS, etc.

Moreover, the required configuration could get complicated depending on the context in which your library is used. As long as your library is the only one needing to map @lib to some path, the mapping that must be added to Webpack (or SystemJS, etc.) can be global. The module bundler or module loader will always replace @lib with your path, which is fine because your package is the only one that needs @lib replaced. However, suppose another library author does exactly what you did, and a consumer of your library also uses that other library. Now you have a situation where @lib must be mapped to one path in some cases, and must be mapped to another path in other cases. This can be configured, but it requires more complex configuration.

I’ve focused on the issue of resolving modules during bundling or when loading them at runtime, but there’s another issue. Consumers would also need to configure their tsc compilation with a special configuration because the .d.ts files

If you just use relative paths in your code then consumers of your library won’t have to worry about adding special configurations to accommodate your library’s special needs.


There’s a special case that may happen to fit your case. If your library is going to be published as midi-lib then you can change your paths map so that instead of @lib/* you have a map for midi-lib/*:

"midi-lib/*": ["projects/midi-lib/src/*"],

(Note that the @ symbol has no special meaning as far as TypeScript is concerned. Also note if your package is meant to be installed with a scope, like @midi-project/midi-lib then you need the scope in the tsconfig.json mapping too: "@midi-project/midi-lib/*": ...)

Basically, the goal here is to set a mapping that allows you to import modules in your project in exactly the same way a consumer of your project would import individual modules from it. If a consumer of your module would import the ParseService with import { ParseService } from "midi-lib/lib/service/parse.service", then in your own code you’d use the same import when you want to use that module. (Note that it does not matter whether you tell consumers to import this module directly. If consumers were to import the module directly, then what path would they use?) So the same path works at compile time and at run time (or bundling time). At compile time, tsc converts the path. At run time or bundling time, Node’s module resolution algorithm (or a tool which can follow the same algorithm, like Webpack or Rollup) converts the path.

How much typing you’d save with this highly depends on the names you’ve chosen and how you structured your library.


In theory, you could have a step after you run ng build that would go over the files produced by ng build and replace @lib in module names with the actual path it is supposed to point to. The problems with this:

  1. It’s not just a matter of running a single tool or flipping a flag in a configuration option. Maybe a tool like rollup can transform the JS files but you need to now learn how it works and write a configuration for it.

  2. AFAIK there’s no readily available tool that will transform the .d.ts files as you need them. You’d most likely have to write your own tool.

  3. You’d also need to patch the AOT compilation metadata generated by the Angular AOT compiler because it also contains module references, and these references are used by consumers of your library. AFAIK, there’s no such tool that exists. So here too you’d have to roll your own.

  4. Your build process could break if a new Angular version changes the format of the AOT compilation metadata or adds a different type of metadata file that needs patching. I know this from experience: I have a couple of packages that are highly experimental Angular applications. For historical reasons, they entirely bypass using Angular CLI for building. Every Angular upgrade from version 4 onwards broke something in the build process of these applications. It often had to do with how AOT compilation metadata was handled.

Leave a Comment

tech