Search code examples
typescriptpackage.json

Not able to get Typescript definitions working when using subpath exports


In my project (latest commit) I have two exports in my package.json:

  "exports": {
    ".": "./dist/index.js",
    "./react-lazy": "./dist/react-lazy.js"
  },

Using this from a JS project works fine, but TS projects complain about not finding the types after I split one module into a subpath export.

I then tried the "expanded" exports version I saw mentioned in another answer:

  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./types/index.d.ts"
    },
    "./react-lazy": {
      "import": "./dist/react-lazy.js",
      "types": "./types/react-lazy.d.ts"
    }
  },

No go.

I then tried an alternative approach using typesVersion:

  "typesVersions": {
    "*": {
      ".": "./types/index.d.ts",
      "./react-lazy": "./types/react-lazy.d.ts"
    }
  },

Also, no go. What gives? Now I am a bit confused as to what is the problem here.

When trying this out, I npm link up this project and npm link @fatso83/retry-dynamic-import from the test project and run tsc --lib es2015,dom --noEmit main.ts.

I am basically just testing out whether these lines passes the compiler or not:

import { dynamicImportWithRetry } from "@fatso83/retry-dynamic-import";

import reactLazy from "@fatso83/retry-dynamic-import/react-lazy";

Currently, all above attempts end up with

$ tsc --lib es2015,dom --noEmit main.ts
main.ts:3:40 - error TS2307: Cannot find module '@fatso83/retry-dynamic-import' or its corresponding type declarations.

3 import { dynamicImportWithRetry } from "@fatso83/retry-dynamic-import";
                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If I was just exporting index.js, all would be good, as this works:

  "exports": "./dist/index.js",
  "types": "./types/index.d.ts",

That will make the first line pass, but not the second.


Solution

  • The answer that got everything working was that TypeScript supports subpath imports IF AND ONLY IF you set moduleResolution to nodeNext or node16.

    This will unfortunately also force you to specify the file extension, for whatever reason, on your dynamic imports (as in import('./foo.ts').

    So the TSC command above was just slightly tweaked:

    $ npx tsc --esModuleInterop true --lib es2015,dom --noEmit --moduleResolution nodeNext main.ts
    

    And then all was working.

    Unfortunately, I am not sure how hot other consumers will think this is ... It means either expose a package that might pull in React transitively (if not using subpath exports), even if not using it, or force consumers to add extensions to their dynamic imports.

    EDIT: Workaround that works in all environments While the above does answer the question of how and why, it ends with a few points of how using the subpaths feature affects clients. Turns out that if the only reason you are using subpath export is that your modules is in a ./dist directory, you can circumvent the whole problem by making the files importable at root, through some adjustments to how you create and publish the package. Here is my setup (tagged to avoid link rot):

    This basically copies package.json, ./dist/* and ./types/* files into a ./pkg folder, and then manually adjusts the type definitions to include the react-specific module.

    While this is not a solution to the problem I asked, it is probably the solution a lot of people really want (and the one I required). You can see my build script also tests consumers, a kind of integration tests where I try consuming my package in a variety of Typescript setups to see that it has no issues, so this really works :)