You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cannot import a package with "type": "module" from cjs even if the package has an exports map #49299
Module 'my-dep' cannot be imported using this construct. The specifier only resolves to an ES module, which cannot be imported synchronously. Use dynamic import instead. ts(1471)
🙂 Expected behavior
TypeScript should be able to find the right import of my-dep from its exports map. The type field shouldn't prevent this.
Extra info
If I do any one of the following, everything works as expected.
specify "type": "module" in my own package.json
remove "type": "module" from my-dep's package.json
change my code's file extension to .mts
No changes to tsconfig.json seem to fix the issue.
I would have expected that the following would have worked but it didn't:
So the issue happening here is that the line "types": "./index.d.ts" declares a types file that is inferred as an ESM module because of the "type": "module", i.e. because of "type": "module" the fact that ./index.d.ts ends with .ts means it gets treated as an ESM module declaration file.
An alternative fix (for the library) is to make ./index.d.ts a .cts file instead, which would be importable by both. Alternatively just providing types for the esm/cjs entry points separately would work i.e.:
While it does seem like it would be reasonable for "types": "./index.d.ts" to apply for both, consider the following declaration:
exportdefaultfunctionfoo(): number;
If this declaration is interpreted as ESM, the intention is clear, however interpreted as CJS, is this supposed to be a CJS module with { default: () => number }? OR is the intention that this behaves like module.exports = foo (in TS syntax export = function foo(): number?
Both interpretations seem reasonable, and there's no way to tell just from the declaration which it is. If the library is actually implemented in TS, then the better thing for the library to do is just to have index.mts and index.cts and let TS generate the index.d.mts and index.d.cts files, if compiled into the same directory they'll be picked up just fine (or if a different directory, use "types": { "import": "...", "require": "..." } as above).
is this supposed to be a CJS module with { default: () => number }? OR is the intention that this behaves like module.exports = foo (in TS syntax export = function foo(): number
TypeScript should just treat it as { default: () => number } as not doing so would break a lot of existing libraries (as this is how things have always worked, up until TS 4.7)
If I hover over foo in the import statement, in vscode; I do get the correct types.
Note that from TS's perspective, there is no CJS module, this is because when "types" is specified TS will look through "exports" find that "my-dep" has a "types": "./index.d.ts" and stop searching.
Even though import foo from "my-dep" is technically require, from TS's perspective the resolution to this is still "./index.d.ts" because it the earliest match for importConditions = ["types", "require", "default"], basically the first entry in "exports" whose keys are all in the given conditions will win. (This is the same reason why if you put "default" first in "exports" it will always win regardless of other entries in "exports".)
Hence it shows you the types for the ES module, but it considers it an error as require will refuse to evaluate ESM which TS thinks is what is going to be require-ed based on the inferred "types" it found.
So basically the issue is that by the dependency specifying "type": "module", the .d.ts file will be interpreted as a module and thus cjs files cannot import it.
I assume the same thing would happen but in reverse if "type": "commonjs" was specified?
So if "type" is not specified, then the .d.ts file is free to be interpreted either way? Thus why removing "type" from the dependency seems to fix the issue.
If this is indeed the case, is it consider safe to take this approach or should a library always give both a .d.mts and a .d.cts file, even if the two files would be identical?
So if "type" is not specified, then the .d.ts file is free to be interpreted either way? Thus why removing "type" from the dependency seems to fix the issue.
No then it will be interpreted as commonjs, however ESM can import so there won't be an error reported.
However, this isn't without hazards, i.e. consider if we had like I suggested above, however we also had an ESM file:
importfoofrom"my-dep";console.log(foo);
Under the CJS resolution the shape of the module is { default: () => number }, so this means the program should prints:
{ default: functionfoo(){ ... }}
This is probably not what was intended, because in ESM we would want the program to print the function itself:
functionfoo(){ ... }
(Note that this is actually what the runtime does, because there actually is an ESM file).
This is unlike prior TS code, because in prior TS code there was esModuleInterop, which would've extracted the default export from the CJS namespace. But actual Node ESM doesn't extract the default export, it always returns the whole CJS namespace.
If this is indeed the case, is it consider safe to take this approach or should a library always give both a .d.mts and a .d.cts file, even if the two files would be identical?
Yes. It is kind've a shame that the files would be identical, although given identical contents is ideal for compression there's not really any loss to it.
With #47807, I believe it's possible to make it work but I haven't tested too much.
Ah yeah, you probably need resolution-mode for this pattern.
Also with regard to index.d.cts in my last comment, is it possible to also export the type Foo while using export = Bar?
Kind've, you can export = a namespace and it'll allow type exports in that namespace. This may require changes to Bar, but namespace can generally merge with most kinds.
As an example if the CJS module.exports was a function then you can do:
And this will allow Bar to be both a function and a namespace with Bar.Foo as a type. (And will even work with import { Foo } from "..." in both ESM and CJS).
Activity
Jamesernator commentedon May 29, 2022
So the issue happening here is that the line
"types": "./index.d.ts"declares a types file that is inferred as an ESM module because of the"type": "module", i.e. because of"type": "module"the fact that./index.d.tsends with.tsmeans it gets treated as an ESM module declaration file.An alternative fix (for the library) is to make
./index.d.tsa.ctsfile instead, which would be importable by both. Alternatively just providing types for the esm/cjs entry points separately would work i.e.:While it does seem like it would be reasonable for
"types": "./index.d.ts"to apply for both, consider the following declaration:If this declaration is interpreted as ESM, the intention is clear, however interpreted as CJS, is this supposed to be a CJS module with
{ default: () => number }? OR is the intention that this behaves likemodule.exports = foo(in TS syntaxexport = function foo(): number?Both interpretations seem reasonable, and there's no way to tell just from the declaration which it is. If the library is actually implemented in TS, then the better thing for the library to do is just to have
index.mtsandindex.ctsand let TS generate theindex.d.mtsandindex.d.ctsfiles, if compiled into the same directory they'll be picked up just fine (or if a different directory, use"types": { "import": "...", "require": "..." }as above).RebeccaStevens commentedon May 29, 2022
If I hover over
fooin the import statement, in vscode; I do get the correct types.RebeccaStevens commentedon May 29, 2022
TypeScript should just treat it as
{ default: () => number }as not doing so would break a lot of existing libraries (as this is how things have always worked, up until TS 4.7)Jamesernator commentedon May 29, 2022
Note that from TS's perspective, there is no CJS module, this is because when
"types"is specified TS will look through"exports"find that"my-dep"has a"types": "./index.d.ts"and stop searching.Even though
import foo from "my-dep"is technicallyrequire, from TS's perspective the resolution to this is still"./index.d.ts"because it the earliest match forimportConditions = ["types", "require", "default"], basically the first entry in"exports"whose keys are all in the given conditions will win. (This is the same reason why if you put"default"first in"exports"it will always win regardless of other entries in"exports".)Hence it shows you the types for the ES module, but it considers it an error as
requirewill refuse to evaluate ESM which TS thinks is what is going to berequire-ed based on the inferred"types"it found.RebeccaStevens commentedon May 29, 2022
So basically the issue is that by the dependency specifying
"type": "module", the.d.tsfile will be interpreted as a module and thus cjs files cannot import it.I assume the same thing would happen but in reverse if
"type": "commonjs"was specified?So if
"type"is not specified, then the.d.tsfile is free to be interpreted either way? Thus why removing"type"from the dependency seems to fix the issue.If this is indeed the case, is it consider safe to take this approach or should a library always give both a
.d.mtsand a.d.ctsfile, even if the two files would be identical?Jamesernator commentedon May 29, 2022
No then it will be interpreted as commonjs, however ESM can import so there won't be an error reported.
However, this isn't without hazards, i.e. consider if we had like I suggested above, however we also had an ESM file:
Under the CJS resolution the shape of the module is
{ default: () => number }, so this means the program should prints:This is probably not what was intended, because in ESM we would want the program to print the function itself:
(Note that this is actually what the runtime does, because there actually is an ESM file).
This is unlike prior TS code, because in prior TS code there was esModuleInterop, which would've extracted the default export from the CJS namespace. But actual Node ESM doesn't extract the default export, it always returns the whole CJS namespace.
Yes. It is kind've a shame that the files would be identical, although given identical contents is ideal for compression there's not really any loss to it.
RebeccaStevens commentedon May 29, 2022
Oki, that makes sense.
What about in the case of a JS library providing it's own type declarations?
Would the following be valid?
index.d.ts:index.d.mts:index.d.cts:Or would all the
.d.tsfiles need to be converted to.d.mtsand.d.cts?Jamesernator commentedon May 30, 2022
Yes, this pattern is fine.
RebeccaStevens commentedon May 30, 2022
James Browning (@Jamesernator) from my testing, it seems to fail. With #47807, I believe it's possible to make it work but I haven't tested too much.
Also with regard to
index.d.ctsin my last comment, is it possible to also export the typeFoowhile usingexport = Bar?Jamesernator commentedon May 30, 2022
Ah yeah, you probably need
resolution-modefor this pattern.Kind've, you can
export =anamespaceand it'll allow type exports in that namespace. This may require changes toBar, butnamespacecan generally merge with most kinds.As an example if the CJS
module.exportswas a function then you can do:And this will allow
Barto be both a function and a namespace withBar.Fooas a type. (And will even work withimport { Foo } from "..."in both ESM and CJS).RebeccaStevens commentedon May 30, 2022
James Browning (@Jamesernator) Thanks for your help with this.