Repository navigation
Improve support for internal packages by resolving path aliasesΒ #58657
Description
π Search Terms
Internal packages
Path alias
Resolve paths
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β Suggestion
Add a tsconfig.json to the exports field in package.json so that internal packages can share their TypeScript config especially their path aliases.
// ./libs/math/package.json
{
"name": "@repo/libs-math",
"version": "1.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
// exports local tsconfig.json so consumers can resolve local path aliases
"./tsconfig.json": "./tsconfig.json",
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
}
}π Motivating Example
Please find a reproduction example here: https://github.com/zirkelc/typescript-internal-packages
Install all dependencies with
pnpm installin the project root. Then openapps/foo/src/index.tsand you will see that TypeScript cannot resolve the dependency to@repo/libs-with-aliascorrectly.
I'm using internal packages to share code in my monorepo. An internal package is package which is NOT published to NPM and which is NOT explicitly compiled with TypeScript before it can be used by other packages. It exports the source TypeScript *.ts files from the package.json via the main, types, and exports fields:
// ./libs/math
{
"name": "@repo/libs-math",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
}
}// ./libs/math/src/index.ts
export const add = (a: number, b: number) => {
return a + b;
};This internal package is meant to be installed by other packages in the same repo:
// ./apps/foo
{
"name": "@repo/apps-foo",
"dependencies": {
"@repo/libs-math": "workspace:*"
}
}And can be used like a normal dependency which resolve to the original uncompiled TypeScript code:
// ./apps/foo/src/index.ts
import { add } from '@repo/libs-math'
// ^? Imports the typescript code from the internal packageEverything good so far. This pattern works well as long as none of the internal packages is using TypeScript path aliases. However, let's re-structure the internal package and use path aliases like ~/math:
// ./libs/math/tsconfig.json
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"paths": {
"~/math": ["./src/math/index.ts"],
},
},
"include": [ "src"]
}The path alias ~/math points to the sub-folder ./src/math/index.ts. The previous ./src/index.ts becomes a simple barrel file exports
// ./libs/math/src/index.ts
export * from '~/math'
// ^? Exports from local path alias pointing to ./libs/math/src/math/index.tsNow, the internal package @repo/libs-math uses path aliases which are defined its own local tsconfig.json. However, the previous import in ./apps/foo will now start to fail:
// ./apps/foo/src/index.ts
import { add } from '@repo/libs-math'
// ^? Module '"@repo/libs-math"' has no exported member 'add'TypeScript doesn't find the function anymore, even though go to definition to @repo/libs-math still works.
I assume the reason is the following: TypeScript resolves the imports starting from the app ./apps/foo/src/index.ts to the internal package ./libs/math/src/index.ts. However, TypeScript still works inside the ./apps/foo/tsconfig.json and is not aware of the path aliases defined by the ./libs/math/tsconfig.json. It would need a way to recognize it is in a different package now and must look for the right tsconfig.json in ./libs/math.
That's where my idea of exporting the tsconfig.json in the package.json comes in. TypeScript would notice it is working in a different package when it was moving from ./apps/foo/src/index.ts to ./libs/math/src/index.ts. Then it sees this new package has a local TypeScript config exported from the local ./libs/math/package.json. It sees the path aliases and applies these aliases for all paths inside ./libs/math/*.
π» Use Cases
There are two possible workarounds:
- global path alias: create a root-level
tsconfig.paths.jsonwhich defines all path aliases for the entire project. Each package-leveltsconfig.jsonextends from this config. There are two disadvantages to this workaround:
- packages cannot define package-level path aliases as they would overwrite the inherited root-level aliases
- each package can import from every other package via path aliases. that means you could accidentally import from the internal package
@repo/libs-mathvia a typescript path alias instead of a node package (workspace) import.
- project references: typescript project references can solve this issue, but they add a lot of complexity due intermediate build steps and overhead in extra multiple
tsconfig.json. The package@repo/apps-foohas a node dependency to@repo/libs-mathviapackage.json. For project references, this dependency must be replicated again in thetsconfig.jsonviareferences. So the dependency graph ofpackage.jsonmust be repeated for every new internal package.
Activity
damianobarbati commentedon Oct 8, 2024
Chris Cook (@zirkelc) I'm not sure if you are dealing with the same problem: I'm trying to NOT use the tsconfig
pathsand use only native module resolution of node.My understanding is that things we can use are:
While the first seems to work with
nodenext, the latter seems to be broken and tsc complains aboutIs there a way to proper deal with this? Having relative imports and avoid using tsconfig paths?
zirkelc commentedon Oct 8, 2024
Damiano (@damianobarbati) indeed my issue is only related to TS and how it resolves the
paths.Sub-path imports are a Node.js feature and I assume they are resolved by Node from within
tsc, butimport typeis TypeScript-specific. I guess when TS seesimport typeit uses its own resolution algorithm to resolve#types/Foeas a path alias.Maybe this will be supported in the future. Here is another issue where you might be able to ask for help: #52460
8ctavio commentedon Oct 29, 2025
I am also interested in a way for monorepo/workspace packages to somehow share module-resolution-related configuration with local consumers.
In my case, I have a pnpm monorepo with an internal package installed by other packages in the monorepo. For the shared package, I use
customConditionsto resolve package.json's"exports"and"imports"entries fromsrc/during development anddist/in production. However, tsc is unaware of the shared package'scustomConditions(or any other module-resolution-related options, likepaths), so it is required to build the shared package to properly consume it, which is especially inconvenient when it is updated.For a possible solution, I think a conservative approach would be to have consumers define an "allowlist" of dependencies in their
node_modulesfor tsc to look for theirtsconfig.json(probably ignoring project references) to use module-resolution-related options likepathsorcustomConditions. For the provided scenario, it would look like this:I opened a feature request to more conveniently define output-to-input directory maps for tsc to resolve output files when they have not been yet generated, which would also benefit from monorepo/workspace packages support.
resucutie commentedon Aug 29, 2026
hello! any progress towards this issue? i'd like to get it resolved!