TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Different behavior for type reference directive in nodenext based on whether types are in @types or not

Open andrewbranch opened this issue 3 years ago • 14 comments

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 trying node_modules/@types/*
  • accepts cts/mts file extensions
  • looks at exports with conditions node, 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)

andrewbranch avatar Jan 14 '22 19:01 andrewbranch