Reputation: 533
With ECMAScript Module support added in Typescript 4.7, several new file extensions may get involved during a TS build including .mjs
, .d.mts
. If a project has it enabled this, TS compiler has more complexity to manage when it does module resolution (locates the files to import). With the new ESM file extensions there are two straightforward kinds of modules:
.js
implementation, .d.ts
declaration file.mjs
implementation, .d.mts
declaration fileNot all packages fit the above categories. Some packages ship with both .js
and .mjs
versions of implementation but just the .d.ts
declaration file, without .d.mts
What are the resolution rules in this case? It seems .mjs
gets prioritised over .js
but refuses to work without .d.mts
which is problematic if you don't own the imported module. Can this be resolved without modifying the package?
For a project that has ESM enabled via the following configs
// package.json
"type": "module"
// tsconfig.json
"module": "Node16",
"moduleResolution": "node16"
that depends on a package (e.g. js-base64) which ships with .js
, .mjs
, .d.ts
but no .d.mts
$ ls -l node_modules/js-base64
base64.d.ts
base64.js
base64.mjs
then when I try to import it like
// myfile.ts
import { Base64 } from 'js-base64'
I get an error:
Could not find a declaration file for module 'js-base64'. '/myproj/node_modules/js-base64/base64.mjs' implicitly has an 'any' type
However, if I do
$ ln -s node_modules/js-base64/base64.d.ts node_modules/js-base64/base64.d.mts
then the error goes away which suggests to me that .d.ts
is deliberately ignored.
Upvotes: 19
Views: 30664
Reputation: 11879
The question above asks about a package.json
file, that AToW (at the time of writing) was configured like this.
{
"main": "base64.js",
"module": "base64.mjs",
"types": "base64.d.ts",
"files": [
"base64.js",
"base64.mjs",
"base64.d.ts"
],
"exports": {
".": {
"import": "./base64.mjs",
"require": "./base64.js"
},
"./package.json": "./package.json"
},
}
When the question's author would import the package file as an ESM module, he would use an import statement to attempt to access it, however, his "TypeScript Compiler" (aka TSC
) would throw the error (displayed below) each time he tried to use the imported module.
Could not find a declaration file for module: "js-base64" '/myproj/node_modules/js-base64/base64.mjs' implicitly has an 'any' type
Can the module resolve without re-configuring the package. And why does creating a symbolic link, as shown is Sergey's question, seem to fix the error that is occurring?
This is a bi-modular Node package, meaning that it contains two builds, a CJS build, and a ESM build. It is configured to use different files only at the entry-point of the module, the rest of the package should (in theory) be the same.
The files configured for the two different entry points can be seen in the package.json
snippet I posted at the very-top of this answer.
Before we get to far, I want to make it clear, the package.json
files configuration has a single issue, however, for the most part, its configured correctly. The ESM
& CJS
entry points & builds all work well, the only problem is that when a TypeScript user imports the module as an ECMAS-Module — aka ESM
— it doesn't resolve typed, consequently, the TypeScript compiler throws a type error when the questions author attempts to import & use the package.
Not resolving with as a typed module has some obvious implications. Although, they are obvious to most, its good to cover them just to make sure all readers are grounded at the same point in the discussion.
If your use pure JavaScript you likely won't notice anything is off. JS doesn't use types, and therefore, importing the package into a pure JavaScript code-base, should be able to be done — as an "ES-Module", or as a "Common-JS Module" — without experiencing any issues.
Those using TypeScript should be able to resolve the module via an import { ... } from 'js-base64'
&/or a require('js-base64')
statement and everything should resolve being typed.
Those trying to import the package using TypeScript — in an ESM environment — will get type errors, and they should notice that the package is not resolving with types.
base64.d.ts
file?So, the package is written in TypeScript, generally a bi-modular package will be written in TypeScript, or at the very least, transpilled using some transpiler. In this case, the package is defiantly a TS authored package. I havn't inspected its code thoroughly but I wouldn't be surprised if it use to be pure JS, and was converted at some point to TS. Either way, it does have a properly TSC emitted .d.ts
declaration file.
It isn't resolving because of how it has been configured. The package is configured, by default, to resolve as a "Common-JS Module". The base package.json
file doesn't contain a "type" field, therefore it defaults to a CJS module. The package.json
file's "type"
field can be set to either...
"commonjs"
(aka CJS) or,"module"
(aka ESM)"type"
field results in the package resolving as a CJS module.Configuring modules for ESM, especially when attempting to support CJS at the same time, has become a very complex ordeal, and currently is not understood by many when it pertains to TypeScript written packages. This is because much of the support for ESM is new, despite the ESM standard being included in the ECMA-262 specification back in 2015.
The important thing to note is that the package-maintainer used the package.json file's "exports" field to define separate entry points for the two module types.
"types": "base64.d.ts",
"exports": {
".": {
"import": "./base64.mjs", // ESM entry point
"require": "./base64.js" // CJS entry point
},
"./package.json": "./package.json"
},
He could have done it the other way around, but he didn't, he did it the way you see above.
The problem can be seen in the snippet above, the types field is where he tells TSC where the declaration file is when the package is imported as a module, the problem is, as I pointed out above, the package is defaulting to resolve as a CJS module, despite defining an ESM entry point.
To solve the issue, the package needs to have a declaration file that has the same exact name & file-extension as the file set as the ESM entry point. Another way to solve the issue or the already existing base64.d.ts
declaration file needs to be explicitly configured to resolve with the file set as the ESM entry point, (which is base64.mjs
).
package.json
file's "exports"
field.// jD3V's adjusted package configuration
{
"main": "base64.js",
"module": "base64.mjs",
"types": "base64.d.ts",
"files": [
"base64.js",
"base64.mjs",
"base64.d.ts"
],
"exports": {
".": {
"import": "./base64.mjs",
"require": "./base64.js",
"types": "./base64.d.ts"
},
},
}
Above you can see that the package is configured such that TypeScript can now infer (from the "types" field that's wrapped in the "exports" field) that the base64.d.ts file should be resolved with all exports.
exports
field like this: "exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": {
// Where TypeScript will look.
"types": "./base64.d.ts",
// Where Node.js will look.
"default": "./base64.mjs"
},
"require": "./base64.js",
},
}
The difference between the JD3V package.json
file, and the TS v4.7 snippet, is that the "import"
field (in the "exports"
field) takes an object as its assigned value, consequently; when the types
location is set in the "TS v4.7" it is setting a specific file for "base64.mjs", where as in the JD3V snippet it sets a specific "types" location for all exports.
package.json
Exports, Imports, and Self-ReferencingUpvotes: 26