TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Self-referencing causes non-portable inferred types (TS2742) false positive

Open gluxon opened this issue 1 month ago • 3 comments

🔎 Search Terms

  • The inferred type of '...' cannot be named without a reference to '...'. This is likely not portable. A type annotation is necessary.
  • TS2742
  • Project reference redirect
  • Self-reference
  • Build mode
  • Watcher

🕗 Version & Regression Information

Versions 4.7 - 5.9.3.

I've reproduced this in TypeScript 4.7.2, which is when self-referencing was first announced as a feature. This bug appears on the latest version of TypeScript at the time of writing (i.e. 5.9.3).

⏯ Playground Link

This issue requires multiple NPM packages. Small repro at https://github.com/gluxon/typescript-portability-error-false-positive-due-to-self-import

💻 Code

I've set up a repro at https://github.com/gluxon/typescript-portability-error-false-positive-due-to-self-import.

The code below is from the repro above and should match exactly. The TypeScript for the repro itself is only ~16 lines of code. In these packages, package a depends on b + c, and package b depends on c.

graph LR
  a --> b
  a --> c
  b --> c
// packages/a/src/index.ts

import { B } from "b";

const b: B = {
  c: { foo: "bar" },
};

export const c = b.c;
// packages/b/src/index.ts

import { C } from "c";

export interface B {
  readonly c: C;
}
// packages/c/src/index.ts

export type { C } from "./C";
export type { C2 as C2 } from "./C2";
// packages/c/src/C.ts

export interface C {
  readonly foo: "bar";
}
// packages/c/src/C2.ts

// 🚨 This self-reference causes the non-portable type false positive. 🚨
// Importing from "./C" instead of "c" fixes the issue.
import { C } from "c";

export type C2 = C;

🙁 Actual behavior

When running tsc with build mode, a false positive error is shown.

tsc --build --verbose packages/a/tsconfig.json
[8:40:22 PM] Building project '/Volumes/git/typescript-false-positive-non-portable-watcher-error/packages/a/tsconfig.json'...

packages/a/src/index.ts:7:14 - error TS2742: The inferred type of 'c' cannot be named without a reference to '../node_modules/c/src'. This is likely not portable. A type annotation is necessary.

7 export const c = b.c;
               ~

The error above should not happen since:

  1. Running tsc without the build flag does not show this issue. (See "Expected Behavior" below.)
  2. The portability error is non-sensical. It's looking for a reference to package c's source code in ../node_modules/c/src rather than its built .d.ts files. I've narrowed this to a bug with project reference redirects.
  3. The portability error happens even when c is a dependency of a.

🙂 Expected behavior

Running tsc without the --build flag results in a successful compilation of package a.

# Compile manually in topological order.
tsc -p packages/c/tsconfig.json
tsc -p packages/b/tsconfig.json
tsc -p packages/a/tsconfig.json

Additional information about the issue

No response

gluxon avatar Nov 26 '25 04:11 gluxon

Adding a bit of usage context — It isn't a high priority for this issue to be fixed from my side.

It used to be a very pressing issue since my team was getting 15+ of these errors from the TypeScript watcher running from tsc --build --watch, which added noise and made it harder to discover real errors. We've since rolled out a lint rule to prevent self-referencing, which eliminated this problem in our codebases.

I wanted to file this issue regardless since other TypeScript developers may benefit from the information in this report. I spent several days debugging the TypeScript compiler to see what was causing the false positives only in tsc --build-watch before realizing it was the self-reference.

Hope this issue report is helpful!

gluxon avatar Nov 26 '25 04:11 gluxon

Adding a bit more debugging notes from when I was looking deep into this. I think there's two potential issues going on:

  1. Self-references in built files resolve to original files instead of built files
  2. Discrepancies in --build mode when resolving symbol chains

1. Self-references in built files resolve to original files instead of built files

When running --traceResolution in the minimal repro

tsc -p packages/a/tsconfig.json --traceResolution

The self-reference in C2.d.ts was resolving to packages/c/src/index.ts.

======== Resolving module 'c' from '/Volumes/git/typescript-false-positive-non-portable-watcher-error/packages/c/dist/C2.d.ts'. ========
Using compiler options of project reference redirect '/Volumes/git/typescript-false-positive-non-portable-watcher-error/packages/c/tsconfig.json'.
Module resolution kind is not specified, using 'NodeNext'.
======== Module name 'c' was successfully resolved to '/Volumes/git/typescript-false-positive-non-portable-watcher-error/packages/c/src/index.ts'. ========

I would have expected it to resolve to packages/c/dist/index.d.ts.

2. Discrepancies in --build mode when resolving symbol chains

When attempting to serialize the problematic inferred type in --build mode:

packages/a/src/index.ts:7:14 - error TS2742: The inferred type of 'c' cannot be named without a reference to '../node_modules/c/src'. This is likely not portable. A type annotation is necessary.

7 export const c = b.c;
               ~

TypeScript iterates through these parentSpecifiers to find a potential import specifier candidate. This is computed in checker.ts#L8405-L8411

parentSpecifiers = parents!.map(symbol =>
    some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)
        ? getSpecifierForModuleSymbol(symbol, context)
        : undefined
);

Build Mode

In --build mode, the parentSpecifiers evaluate to:

❯ parentSpecifiers
0 = '../node_modules/c/src/C'
1 = '../node_modules/c/src/C'
2 = '../node_modules/c/src/C'
3 = '../node_modules/c/src'
4 = '../node_modules/c/src'

CLI

However, without build mode, the parentSpecifiers show:

❯ parentSpecifiers
0 = '../node_modules/c/src/C'
1 = '../node_modules/c/src/C'
2 = '../node_modules/c/src'
3 = 'c'

It seems in --build mode, the option for just c as the module specifier is not available. The --build mode only knows about the non-portable specifiers in ../node_modules/c/src.

No self-reference

For completeness, if you remove the self-reference, the parentSpecifiers look correct and don't reference c/src ever.

❯ parentSpecifiers
0 = '../node_modules/c/dist/C'
1 = '../node_modules/c/dist/C'
2 = 'c'

gluxon avatar Nov 26 '25 04:11 gluxon

🤖 Thank you for your issue! I've done some analysis to help get you started. This response is automatically generated; feel free to 👍 or 👎 this comment according to its usefulness.

Similar Issues

Here are the most similar issues I found

If your issue is a duplicate of one of these, feel free to close this issue. Otherwise, no action is needed.

RyanCavanaugh avatar Dec 01 '25 18:12 RyanCavanaugh