TypeScript: Surprising (or incorrect) priorities with package.json exports fields?

I’m trying a scenario of module: nodenext with Vue.js. I hit a few issues with resolution of declaration files.

Here’s the current Vue.js declarations package.json:

{
    "name": "vue",
    "version": "3.2.20",
    "description": "The progressive JavaScript framework for buiding modern web UI.",
    "main": "index.js",
    "module": "dist/vue.runtime.esm-bundler.js",
    "types": "dist/vue.d.ts",
    // ...
    "exports": {
      ".": {
        "import": {
          "node": "./index.mjs",
          "default": "./dist/vue.runtime.esm-bundler.js"
        },
        "require": "./index.js"
      },
      // ...
      "./package.json": "./package.json"
    }
    // ...
}

However, referencing this in a project results in the following error:

import * as Vue from "vue";
//                   ~~~~~
// error
// Could not find a declaration file for module 'vue'. 'USER_DIR/hackathon/vue-proj/node_modules/vue/index.js' implicitly has an 'any' type.
//  Try `npm i --save-dev @types/vue` if it exists or add a new declaration (.d.ts) file containing `declare module 'vue';`

This kind of makes sense - I think you could argue that this isn’t configured right for moduleResolution: node12 or later.

I was able to get this working by adding

 "exports": {
     ".": {
     "import": {
         "node": "./index.mjs",
         "default": "./dist/vue.runtime.esm-bundler.js"
     },
     "require": "./index.js",
+    "types": "./dist/vue.d.ts"
     },
 }

But the following DID NOT work.

 "exports": {
     ".": {
     "import": {
         "node": "./index.mjs",
         "default": "./dist/vue.runtime.esm-bundler.js"
+        "types": "./dist/vue.d.ts"
     },
     "require": "./index.js",
     },
 }

That part seems like a bug, right?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 5
  • Comments: 26 (18 by maintainers)

Commits related to this issue

Most upvoted comments

Likely there should be a warning if anything comes after a “default” condition.

types is a TS-specific main, which exports blocks - I don’t see why it shouldn’t also block types.

I think this is fair, but it highlights 3 things to me

  1. We should really give a more accurate error message

    “A ‘types’ field was found in this package’s ‘package.json’, but was not used because an ‘exports’ field was found and took priority over the top-level ‘main’ and ‘types’ field.”

    Probably needs to be word-smithed, but I think it would be helpful.

  2. We need to be cautious in our messaging - over-eager people will try to use this in regular projects, and existing packages aren’t ready to accommodate them.

  3. We probably should help package authors get ready for node12+ resolution modes.

Declaring types but not exports.{something}.types is basically always an error, right?

First of all, note that if main, or any part of exports points to a JavaScript file, and there is no corresponding types key when resolving through that package.json path, the compiler will look for a declaration file next to that JavaScript file and use that. So a foolproof way to write a valid project structure and package.json file is just to publish your declaration files in the same place as their partner JS files and literally never write types anywhere in your package.json. The only time an author has to start writing types keys is if they break that structure, e.g. by putting JS in one directory and types in another. (This seems to be one of package authors’ absolute favorite ways to make their own lives harder. Maybe it’s the default for some third party tool like rollup?)

To answer your other questions:

In node16/nodenext, it is expected for the top-level types to be ignored in the presence of exports, because Node ignores the top-level main in the presence of exports. (The point of a targeted moduleResolution is to maintain a parallel logic like this as perfectly as we can, so there is parity between what works at compile time and what works at runtime.)

Is there some mechanism for me to depend on package foo but tell TS to either ignore its exports field, or override it in some way?

Nothing specifically for this problem, but tsconfig paths will probably let you hack something together?

does that just mean I have to get the library authors to fix their package

Please 🙏

but bunding TS for browser consumption through webpack

My take is that it’s basically a coincidence that TypeScript worked reasonably well with bundlers for years without complaints. Part of the promise of bundlers was that you can develop your frontend projects like Node, using dependencies from npm, and so bundlers copied Node’s module resolution strategy, so --moduleResolution node was appropriate for TS, and then enhanced it in ways that TypeScript could sometimes model with paths or pattern ambient modules and otherwise felt out of scope. But now, Node and bundlers have both diverged away from --moduleResolution node in meaningful ways and different directions. We shipped support for the direction Node went, but we effectively now have no support for what bundlers are doing. This is 90% of what I have been thinking about and working on for the last month. Hoping to publish a proposal this week. So no, this is not the right issue to address that, but there isn’t really a canonical issue for it. Stay tuned.

Also, I think this issue can be closed?

^ fixed by #47007