Skip to content

Improve support for internal packages by resolving path aliasesΒ #58657

Open

Description

@zirkelc

πŸ” Search Terms

Internal packages
Path alias
Resolve paths

βœ… Viability Checklist

⭐ 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 install in the project root. Then open apps/foo/src/index.ts and you will see that TypeScript cannot resolve the dependency to @repo/libs-with-alias correctly.


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 package

Everything 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.ts

Now, 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:

  1. global path alias: create a root-level tsconfig.paths.json which defines all path aliases for the entire project. Each package-level tsconfig.json extends 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-math via a typescript path alias instead of a node package (workspace) import.
  1. 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-foo has a node dependency to @repo/libs-math via package.json. For project references, this dependency must be replicated again in the tsconfig.json via references. So the dependency graph of package.json must be repeated for every new internal package.

Activity

  1. damianobarbati commented on Oct 8, 2024

    @damianobarbati

    Chris Cook (@zirkelc) I'm not sure if you are dealing with the same problem: I'm trying to NOT use the tsconfig paths and use only native module resolution of node.

    My understanding is that things we can use are:

    • subpath import: `import Foe from '#api/Foe'
    • module alias import: `import type Foe from '#types/Foe'

    While the first seems to work with nodenext, the latter seems to be broken and tsc complains about

    error TS2307: Cannot find module '@ts-repro/types/User' or its corresponding type declarations.

    Is there a way to proper deal with this? Having relative imports and avoid using tsconfig paths?

  2. zirkelc commented on Oct 8, 2024

    @zirkelc
    Author

    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, but import type is TypeScript-specific. I guess when TS sees import type it uses its own resolution algorithm to resolve #types/Foe as 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

  3. 8ctavio commented on Oct 29, 2025

    @8ctavio

    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 customConditions to resolve package.json's "exports" and "imports" entries from src/ during development and dist/ in production. However, tsc is unaware of the shared package's customConditions (or any other module-resolution-related options, like paths), 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_modules for tsc to look for their tsconfig.json (probably ignoring project references) to use module-resolution-related options like paths or customConditions. For the provided scenario, it would look like this:

    // ./apps/foo/tsconfig.json
    {
      "compilerOptions": {
        // when importing from "@repo/libs-math",
        // look for ./libs/math/tsconfig.json
        // to use module-resolution-related options
        "<monorepo-dependencies>": ["@repo/libs-math"] // could also support patterns: "@repo/*"
      }
    }

    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.

  4. resucutie commented on Aug 29, 2026

    @resucutie

    hello! any progress towards this issue? i'd like to get it resolved!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions