Repository navigation
package.json exports resolution uses fallback conditions, unlike Node #50762
Description
Activity
Thanks for reporting this! I think I ran into this while testing the types for a library I had created, but honestly thought it was more something I was doing wrong than something TS would be doing wrong.
Here's an additional library I've found that has
typesat the end instead of at the start:headless-ui/react and headless-ui/vue
There's probably more out there as well.
As far as whether to fix it or not:
The pro of fixing it now is that it prevents the ecosystem from continuing this behavior, and thus allowing it to be entrenched.
Maybe at the least there could be a warning saying "this works now, but it's incorrect and may not work in the future" or something? 🤷
Reacted by Oskar Larsson Högfeldtandrewbranch commented
on Sep 13, 2022 MemberAuthorMore actionsMaybe at the least there could be a warning saying "this works now, but it's incorrect and may not work in the future" or something?
We don’t have a way to do warnings on the CLI, but #46861 is related.
Reacted by Anthony FrehnerAndarist commented
on Sep 19, 2022 ContributorMore actionspackage.json
{ "exports": { ".": { "require": "./dist/main.js", "types": "./dist/foo.d.ts" } } }pkg layout
dist/main.js dist/main.d.ts dist/foo.d.ts package.jsonWhich
.d.tsshould be loaded? Should it bedist/main.d.tsordist/foo.d.ts? Is a sibling.d.tslookup still a thing when dealing withexports?Reacted by Anthony Frehner and Bailey Wickhamandrewbranch commented
on Sep 19, 2022 MemberAuthorMore actionsYes, sibling
.d.tslookup is always a thing, and it’s always the way I recommend people to structure packages when possible. In your example,main.d.tswould be looked up if the import was CJS-format;foo.d.tswould be looked up if the import was ESM-format.Andarist commented
on Sep 20, 2022 ContributorMore actionsYes, sibling .d.ts lookup is always a thing, and it’s always the way I recommend people to structure packages when possible.
Interesting. What's the argument for that? I usually assume that an explicit
"types"might be better as it's more discoverable without the additional FS check/crawl (for instance npm is labeling packages with the TS label, I didn't check how they do it exactly but I wouldn't be surprised if they would only check thepackage.jsoncontent).As to the original issue, IMHO this should be fixed on a correctness basis - to match the node's algorithm. Other tools are implementing their algorithm so having this bug~ here might cause people hard-to-debug issues when types will be found by TS itself but won't be found by other tools.
Reacted by Anthony Frehnerandrewbranch commented
on Sep 20, 2022 MemberAuthorMore actionsI don’t mind if people want to explicitly list their types, but I dislike separating all the types into a different folder because it’s just so easy to mess up in subtle ways. Putting your .d.ts files next to your .js files is foolproof. Unfortunately it seems library authors by and large dislike this for some reason, and it causes all kinds of problems. So I recommend putting .d.ts files with their partners, for the sake of simplicity and explainability, and then if you want to add (strictly unnecessary)
typeskeys to your package.json too, go for it.One reason that library authors do it is because it is very easy to create conflicts between
esmandcjsfiles and their types. I kind of alluded to that in the comment here, and especially since, as you mentioned above, the types foresmandcjsaren't always interchangeable.Another pro of separating them out, though, is that the build output can also include other types of bundles - a "production" and "development" bundle, for example, where the types are the exact same but internally there's additional logging or other tools. Creating a matrix of
esm|cjs|umdwithdevelopment|productionbundle outputs, all with the same types, just seems nicer to have a single types output folder to reduce the number of types that need to be generated, and prevent conflicts between them.Reacted by Andrew Branch and Darryl NoakesAndarist commented
on Sep 21, 2022 ContributorMore actionsYeah, that's the main use case that comes to my mind. It's somewhat easy~ to create a sibling file for your
package.json#mainbut once you start adding more "conditions" etc then we usually need a way to redirect them to that single set of.d.tsfiles (where other conditions in the past were just not observed by TS, so it wasn't as much of an issue).But when parallelizing bundling and type generation, I would usually be wary of writing to the same directory at once. Writing to separate ones and using
package.json#typesseems to be a safer/more straightforward way.But anyway... this all was quite offtopic. Thank you for sharing your thoughts though!
Reacted by Anthony FrehnerHere is another repro, was just trying to debug this for pothos
https://github.com/hayes/ts-esm-cjs-nodenext
So far the only workaround I have found is creating separate definition files for each build directory. Currently we publish a
libdirectory with cjs, anesmdirectory with esm, and adtsdirectory with definitions. To make this work, I would have to remove "types" from pacakge.json, and publish definitions in both thelibandesmdirectories, as well as continuing to publish indtsfor backwards compatability. Would be awesome if there was a better option that didn't require publishing 3 identical definition files for every source fileHere is another repro, was just trying to debug this for pothos
https://github.com/hayes/ts-esm-cjs-nodenext
So far the only workaround I have found is creating separate definition files for each build directory. Currently we publish a
libdirectory with cjs, anesmdirectory with esm, and adtsdirectory with definitions. To make this work, I would have to remove "types" from pacakge.json, and publish definitions in both thelibandesmdirectories, as well as continuing to publish indtsfor backwards compatability. Would be awesome if there was a better option that didn't require publishing 3 identical definition files for every source fileIf it helps, I hacked it so that my build step makes a copy of my
index.d.ts, renames the copy toindex.d.cts, and then my package exports for"require"'s"types"points to that file. Everything works, and the only extra copy I have is aindex.d.cts. 😬I'm sure there's probably going to be issues with it, but it seems better than the alternatives at the moment.
Andarist commented
on Sep 28, 2022 ContributorMore actionsMichael Hayes (@hayes) I was just working around this exact issue earlier today. You can check out what we have settled on at the end in this PR: https://github.com/alexreardon/tiny-invariant/pull/150/files
With the
typescondition being used first it's implied that this package is treated as a CJS package (it has nopackage.json#typeset). And importing default export from a CJS package doesn't work in node - it returns the namespace object instead of the default export.Reacted by Andrew Branch, cqh and Mitchell KemberReacted by Anton Gilgurandrewbranch commented
on Sep 28, 2022 MemberAuthorMore actionsThat’s correct; https://github.com/hayes/ts-esm-cjs-nodenext is unrelated to this issue.
Reacted by Michael Hayes19 remaining items
- addedBreaking ChangeWould introduce errors in existing codeWould introduce errors in existing codeCommittedThe team has roadmapped this issueThe team has roadmapped this issue
on Aug 5, 2025 - addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Aug 20, 2025 - addedDomain: Node ESMLike ES Modules, but specific to Node.js support (cts, cts, mjs, mts)Like ES Modules, but specific to Node.js support (cts, cts, mjs, mts)
on Oct 16, 2025 - addedWon't FixThe severity and priority of this issue do not warrant the time or complexity needed to fix itThe severity and priority of this issue do not warrant the time or complexity needed to fix itand removedCommittedThe team has roadmapped this issueThe team has roadmapped this issueFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Jan 22, 2026 andrewbranch commented
on Jan 22, 2026 MemberAuthorMore actionsAfter a lot of trying, #62483 was all we could do on this without breaking things more than it was worth it.
- added a commit that references this issue
on Jul 2, 2026 - locked as resolved and limited conversation to collaborators
on Jul 22, 2026
Bug Report
🔎 Search Terms
I’ve never been more confident, without searching, that I’m the first person to notice this
💻 Code
{ "exports": { ".": { "import": "./dist/main.mjs", "types": "./dist/foo.d.ts" } } }🙁 Actual behavior
Importing this package from an ESM file, in
--moduleResolution nodenext, searches for types at these locations:🙂 Expected behavior
It should not search the
typescondition, becauseimportalready matched, and contained a valid target./dist/main.mjs. Resolution here should fail for consistency with Node, which would throw a resolution error if it matched a condition but then failed to find the file specified in it.Fixing this bug may cause more harm than good. I noticed it because I claimed that a popular library would be broken under certain conditions due to misconfigured
exports, but was proven wrong in reality. I figured this may be worth documenting, but likely not worth fixing. But if someone can make a good case for why it matters, I would reconsider.