🔎 Search Terms
TS2883, TS2742, cannot be named without a reference, not portable, declaration emit module specifier, exports map, pnpm, import type cannot be resolved, skipLibCheck any
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about declaration emit and "cannot be named"
Tried: 6.0.3, 7.0.2, 7.1.0-dev.20261002.1.
⏯ Playground Link
No response (needs a node_modules layout)
💻 Code
A strict node_modules layout, as pnpm makes it. a depends on s, s depends on c, and a cannot resolve c. The package c has an exports map with a subpath.
node_modules/.store/c/node_modules/c/ package.json with "exports": { "./Thing": … }, dist/Thing.d.ts
packages/s/node_modules/c -> the store
packages/a/node_modules/s -> ../../s
packages/u/node_modules/a -> ../../a
// c/dist/Thing.d.ts
export interface Thing {
readonly x: number;
}
export declare const create: () => Thing;
// packages/s/src/index.ts (composite project)
import { create } from "c/Thing";
export const made = create();
// packages/a/src/index.ts (composite project, references s)
import { made } from "s";
export const thing = { made };
// packages/u/src/index.ts (references a, skipLibCheck)
import { thing } from "a";
export const wrong: string = thing.made.x;
Script that writes the project in a temporary directory and runs tsc -b (argument: the tsc command)
#!/bin/sh
# usage: sh repro-1-import-out-of-reach.sh [tsc command]
set -u
tsc=${1:-tsc}
cd "$(mktemp -d)" || exit 1
# The physical path: on macOS the temporary directory is behind a symbolic link.
cd "$(pwd -P)" || exit 1
mkdir -p node_modules/.store/c/node_modules/c/dist packages/s/src packages/s/node_modules \
packages/a/src packages/a/node_modules packages/u/src packages/u/node_modules
cat >node_modules/.store/c/node_modules/c/package.json <<'JSON'
{
"name": "c",
"version": "1.0.0",
"type": "module",
"exports": {
"./Thing": { "types": "./dist/Thing.d.ts", "default": "./dist/Thing.js" }
}
}
JSON
cat >node_modules/.store/c/node_modules/c/dist/Thing.d.ts <<'TS'
export interface Thing {
readonly x: number;
}
export declare const create: () => Thing;
TS
ln -s ../../../node_modules/.store/c/node_modules/c packages/s/node_modules/c
ln -s ../../s packages/a/node_modules/s
ln -s ../../a packages/u/node_modules/a
library='{
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"rootDir": "src",
"outDir": "out"
},
"include": ["src"]REFERENCES
}'
echo '{ "name": "s", "version": "1.0.0", "exports": { ".": "./src/index.ts" } }' >packages/s/package.json
printf '%s\n' "$library" | sed 's/REFERENCES//' >packages/s/tsconfig.json
cat >packages/s/src/index.ts <<'TS'
import { create } from "c/Thing";
export const made = create();
TS
echo '{ "name": "a", "version": "1.0.0", "exports": { ".": "./src/index.ts" } }' >packages/a/package.json
printf '%s\n' "$library" | sed 's#REFERENCES#,\n "references": [{ "path": "../s" }]#' >packages/a/tsconfig.json
cat >packages/a/src/index.ts <<'TS'
import { made } from "s";
export const thing = { made };
TS
cat >packages/u/tsconfig.json <<'JSON'
{
"compilerOptions": {
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src"],
"references": [{ "path": "../a" }]
}
JSON
cat >packages/u/src/index.ts <<'TS'
import { thing } from "a";
export const wrong: string = thing.made.x;
TS
echo "a depends on s; s depends on c; a cannot resolve c."
echo "u assigns a number to a string, so the build must fail."
$tsc -b packages/u
echo "exit code: $?"
echo "packages/a/out/index.d.ts:"
cat packages/a/out/index.d.ts
🙁 Actual behavior
tsc -b packages/u exits with 0. The declaration of a is:
export declare const thing: {
made: import("c/Thing").Thing;
};
c/Thing cannot be resolved from packages/a. The consumer u reads Thing as any, so const wrong: string = thing.made.x is accepted although x is a number. skipLibCheck hides the failed import; with skipLibCheck: false the same build reports packages/a/out/index.d.ts(2,18): error TS2307: Cannot find module 'c/Thing'.
🙂 Expected behavior
TS2883 at thing in packages/a/src/index.ts. That is what all three versions report for the same layout when c has "types": "index.d.ts" and no exports map:
packages/a/src/index.ts(3,14): error TS2883: The inferred type of 'thing' cannot be named without a reference to 'Thing' from '../../s/node_modules/c'. This is likely not portable. A type annotation is necessary.
Additional information about the issue
- In 7.0.2,
tryGetModuleNameAsNodeModule (internal/modulespecifiers/specifiers.go, about line 786) returns the specifier as soon as it comes verbatim from the package's exports (if verbatimFromExports { return moduleFileToTry }). That return is before the check that the top-level node_modules directory is an ancestor of the importing file, which is the check that gives an empty result, and so TS2883, in the case with no exports map.
- How we found it: a workspace package had
import("@effect/sql-pg/PgClient").PgClient in a declaration and did not depend on @effect/sql-pg. Its consumers read a layer's requirements as any.
🔎 Search Terms
TS2883, TS2742, cannot be named without a reference, not portable, declaration emit module specifier, exports map, pnpm, import type cannot be resolved, skipLibCheck any
🕗 Version & Regression Information
Tried: 6.0.3, 7.0.2, 7.1.0-dev.20261002.1.
⏯ Playground Link
No response (needs a
node_moduleslayout)💻 Code
A strict
node_moduleslayout, as pnpm makes it.adepends ons,sdepends onc, andacannot resolvec. The packagechas anexportsmap with a subpath.Script that writes the project in a temporary directory and runs
tsc -b(argument: the tsc command)🙁 Actual behavior
tsc -b packages/uexits with 0. The declaration ofais:c/Thingcannot be resolved frompackages/a. The consumerureadsThingasany, soconst wrong: string = thing.made.xis accepted althoughxis a number.skipLibCheckhides the failed import; withskipLibCheck: falsethe same build reportspackages/a/out/index.d.ts(2,18): error TS2307: Cannot find module 'c/Thing'.🙂 Expected behavior
TS2883 at
thinginpackages/a/src/index.ts. That is what all three versions report for the same layout whenchas"types": "index.d.ts"and noexportsmap:Additional information about the issue
tryGetModuleNameAsNodeModule(internal/modulespecifiers/specifiers.go, about line 786) returns the specifier as soon as it comes verbatim from the package'sexports(if verbatimFromExports { return moduleFileToTry }). That return is before the check that the top-levelnode_modulesdirectory is an ancestor of the importing file, which is the check that gives an empty result, and so TS2883, in the case with noexportsmap.import("@effect/sql-pg/PgClient").PgClientin a declaration and did not depend on@effect/sql-pg. Its consumers read a layer's requirements asany.