Skip to content

TypeScript 7 declaration emit reuses an unrelated JSDoc import and generates an invalid type reference #63705

Description

@platypii

Search Terms

declaration emit, JSDoc, @import, private import, wrong module specifier, TS2694, symlink

Version & Regression Information

  • Reproduces with TypeScript 7.0.2.
  • Reproduces on the current native compiler main branch at 5b1047d10d32e7d5b446be4de56b126ff42f82bb.
  • Does not reproduce with TypeScript 6.0.3.
  • This is therefore a regression between TypeScript 6.0.3 and 7.0.2.

Reproduction

https://github.com/platypii/typescript-7-jsdoc-private-alias-repro

npm ci
npm run build  # succeeds
npm run check  # fails with TS2694

There is no Playground link because the reproduction depends on declaration emit across multiple modules and a physically installed dependency.

Code

/**
 * @import {SchemaElement} from 'external'
 * @import {Writer} from '../src/types.js'
 */

export class Example {
  /**
   * @param {object} options
   * @param {SchemaElement[]} options.schema
   */
  constructor({ schema }) {
    this.schema = schema
  }
}

src/types.d.ts imports the same external type privately:

import type { SchemaElement } from 'external'

export interface Writer {}

The JavaScript is compiled with allowJs, checkJs, declaration, and emitDeclarationOnly enabled.

Actual behavior

TypeScript 7 emits:

export declare class Example {
  schema: import('../src/types.js').SchemaElement[]
}

The generated declaration is invalid because src/types.d.ts does not export SchemaElement. A consumer reports:

TS2694: Namespace '.../src/types' has no exported member 'SchemaElement'.

The Writer import is otherwise unused. Removing only that JSDoc import causes TypeScript 7 to emit a valid reference to the external package instead.

Expected behavior

Declaration emit should reference the public source of the type, for example:

schema: import('external/src/types.js').SchemaElement[]

At minimum, declaration emit should not select an alias that is not exported from the referenced module.

Additional investigation

I traced the 7.0.2 compiler and found that the node builder selects the correct symbol from node_modules/external; the incorrect path is introduced later while generating the module specifier.

For a physically installed package, ResolvePackageDirectory returns an empty OriginalPath. GetSymlinkCache then combines that empty value with package.json, and ProcessResolution resolves the resulting relative path against the project directory. This records a false symlink mapping from the project root to node_modules/external. The module specifier generator consequently treats the project's src/types.d.ts as an alternative source file and reuses the unrelated JSDoc module specifier.

Guarding that path with packageResolution.OriginalPath != "" prevents the false mapping and produces the valid declaration in this reproduction.

The reproduction's .npmrc only makes its local file: fixture install as a physical directory, matching an ordinary registry-installed dependency. Users do not need that setting to encounter the bug in a real project; the README explains this in more detail.

Metadata

Metadata

Assignees

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions