TypeScript
TypeScript copied to clipboard
Different behavior for type reference directive in nodenext based on whether types are in @types or not
Bug Report
🔎 Search Terms
type reference directive node12 nodenext
💻 Fourslash server test case
I used fourslash server tests to verify this because it treats files in node_modules as a real tsc run would, whereas the compiler test suite treats them as if they’re root files for compilation, which messes up what I’m trying to demonstrate.
I have two dependencies in node_modules here that are structurally identical, but one is inside @types
and the other is not. The one in @types
is resolved by the primaryLookup
of resolveTypeReferenceDirective
in moduleNameResolver.ts, which
- only looks in
typeRoots
(including@types
) - only accepts
.ts
and.d.ts
files (not the cts/mts variants) - does not look at
exports
The one outside of @types
, by merit of not being in typeRoots
, uses the secondaryLookup
function, which uses a very different algorithm that brings in some Node12/NodeNext resolution features. It
- looks in
node_modules/*
first before tryingnode_modules/@types/*
- accepts cts/mts file extensions
- looks at
exports
with conditionsnode, require, types
This test case shows how this discrepancy can make a difference in resolution based only on whether the package is inside typeRoots
or not.
I don’t know what the expected behavior is here, but it feels like it should probably be the same whether the package is in @types
or regular node_modules
.
// @Filename: /tsconfig.json
//// {
//// "compilerOptions": {
//// "module": "nodenext",
//// "types": ["inside-at-types", "outside-at-types"]
//// }
//// }
// @Filename: /node_modules/@types/inside-at-types/package.json
//// {
//// "name": "@types/inside-at-types",
//// "version": "1.0.0",
//// "types": "./index.d.ts",
//// "exports": {
//// ".": {
//// "default": "./main.mjs"
//// }
//// }
//// }
// @Filename: /node_modules/@types/inside-at-types/index.d.ts
//// export {};
//// declare global {
//// var typesFieldInsideAtTypes: any;
//// }
// @Filename: /node_modules/@types/inside-at-types/main.d.mts
//// export {};
//// declare global {
//// var exportsInsideAtTypes: any;
//// }
// @Filename: /node_modules/outside-at-types/package.json
//// {
//// "name": "outside-at-types",
//// "version": "1.0.0",
//// "types": "./index.d.ts",
//// "exports": {
//// ".": {
//// "default": "./main.mjs"
//// }
//// }
//// }
// @Filename: /node_modules/outside-at-types/index.d.ts
//// export {};
//// declare global {
//// var typesFieldOutsideAtTypes: any;
//// }
// @Filename: /node_modules/outside-at-types/main.d.mts
//// export {};
//// declare global {
//// var exportsOutsideAtTypes: any;
//// }
// @Filename: /index.ts
//// // One pair of these should be resolved;
//// // the other unresolved. Currently, they're mixed.
////
//// typesFieldInsideAtTypes;
//// typesFieldOutsideAtTypes;
//// exportsInsideAtTypes;
//// exportsOutsideAtTypes;
goTo.file("/index.ts");
verify.baselineSyntacticAndSemanticDiagnostics();
🙁 Actual behavior
exportsInsideAtTypes
and typesFieldOutsideAtTypes
are unresolved (meaning inside @types
resolves to the types
field while outside @types
resolves to the exports
field)
🙂 Expected behavior
Either both typesFieldInsideAtTypes
and typesFieldOutsideAtTypes
or both exportsInsideAtTypes
and exportsOutsideAtTypes
should be unresolved, while the other pair is resolved.
(@DanielRosenwasser @weswigham)