Handling TypeScript Path Aliases in Published Packages
Prevent leaked TypeScript path aliases in published npm packages. Transform @/ and ~ aliases at build time with tsup path rewriting and validate with publint before publishing.
The paths Directive Runtime Resolution Gap
TypeScript’s compilerOptions.paths mapping operates exclusively at compile-time. It instructs the type checker to resolve virtual module paths but does not emit corresponding runtime require() or import statements. When a package is published, Node.js and modern bundlers bypass tsconfig.json entirely, relying instead on standard module resolution fallback chains. Consequently, any leaked @/ or ~ aliases in the compiled dist/ output trigger immediate runtime failures.
Exact Error / Flag Interaction:
Error: Cannot find module '@/internal/utils' or its corresponding type declarations. / `--noResolve` flag interaction
The resolver cannot map the abstract alias to a physical file path, causing consumer imports and CI pipeline validation to fail.
Build-Time Alias Transpilation with tsconfig-paths
To prevent alias leakage, implement a pre-publish build hook that rewrites path mappings to relative paths during the tsc execution phase. Tools like tsconfig-paths/register or ts-patch intercept the compilation process, transforming abstract aliases into concrete relative imports before files are emitted.
Configure moduleResolution: 'bundler' in your build configuration to enforce strict alias validation without falling back to node_modules. Isolate this resolution strictly to the build step to prevent consumer environment pollution.
Exact Error / Flag Interaction:
TS2307: Cannot find module '@/types/config' or its corresponding type declarations. / `--paths` compiler flag behavior
This surfaces when the compiler lacks a registered plugin to transform the alias during emission or when strict validation flags are misaligned.
Replacing Aliases with Node.js exports Field Mapping
For cross-environment compatibility, transition from TypeScript-specific path aliases to standardized package.json subpath exports. Explicitly map internal source directories to exports keys (e.g., "./utils/*": "./dist/utils/*.js"). Leverage conditional exports ("import" vs "require") to handle dual ESM/CJS distribution without alias conflicts.
Crucially, remove baseUrl and paths from the shipped tsconfig.json to enforce strict consumer resolution boundaries and align with modern compiler pipeline configuration standards. Consumers will resolve imports directly through Node’s native export map, eliminating runtime ambiguity.
Exact Error / Flag Interaction:
ERR_PACKAGE_PATH_NOT_EXPORTED: Package subpath './utils' is not defined by "exports" / `--experimental-specifier-resolution` deprecation
This occurs when legacy alias resolution attempts to bypass the exports field, compounded by deprecated Node.js specifier resolution flags.
Step-by-Step Resolution
- Install and configure
tsconfig-pathsas a dev dependency for build-time alias rewriting.
npm i -D tsconfig-paths && echo '{"compilerOptions":{"plugins":[{"transform":"tsconfig-paths/lib/plugin"}]}}' >> tsconfig.build.json
- Execute
tscwith the--pathsflag disabled and enforce relative path emission in the output directory.
tsc --project tsconfig.build.json --noEmit false --declaration --outDir dist --moduleResolution node16
- Define explicit subpath exports in
package.jsonto replace@/aliases for consumer resolution.
echo '{"exports":{"./utils/*":{"import":"./dist/utils/*.js","require":"./dist/utils/*.cjs"},"./types/*":{"import":"./dist/types/*.js","require":"./dist/types/*.cjs"}}}' >> package.json
- Validate the published artifact by packing locally and installing into a fresh consumer project.
npm pack && cd ../test-consumer && npm install ../my-lib-1.0.0.tgz && node -e "require('my-lib/utils/helpers')"