next.js
next.js copied to clipboard
[NEXT-779] next/* - Typescript cannot find module when moduleResolution=nodenext and type=module
Verify canary release
- [X] I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 21.6.0: Thu Sep 29 20:13:56 PDT 2022; root:xnu-8020.240.7~1/RELEASE_ARM64_T6000
Binaries:
Node: 16.17.0
npm: 8.15.0
Yarn: N/A
pnpm: 7.25.0
Relevant packages:
next: 13.1.7-canary.18
eslint-config-next: 13.0.0
react: 18.2.0
react-dom: 18.2.0
Which area(s) of Next.js are affected? (leave empty if unsure)
TypeScript
To Reproduce
-
pnpm create next-app
(with typescript) - add
"type": "module"
topackage.json
- change
moduleResolution
tonodenext
- run
npx tsc --noEmit
Describe the Bug
Typescript complain about next/*
, for example:
Cannot find module 'next/head' or its corresponding type declarations.
I believe this is because there is no exports
field in node_modules/next/package.json
.
Expected Behavior
Typescript shouldn't complain.
Context
- We use
"type": "module"
as we want repo scripts to be esm rather than cjs. - We use
"moduleResolution": "nodenext"
as we use a setup similar to the monorepo created bynpx create-turbo@latest
, but we want sources to be undersrc
and not the root folder.
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
No response
Potential fix https://github.com/vercel/next.js/issues/46676#issuecomment-1451720255
Doesn't look like it:

And even if you remove the other export
statement:

Interesting, it worked for me in a "module": "node16"
app (this is in tsconfig.json
), as noted in #46676 (ESM, also using "type": "module"
in package.json
)
Maybe @andrewbranch has a tip here... 🤔
Or maybe this issue is not a duplicate of #46676 cc @balazsorban44
Someone correct me if I'm wrong, but on the latest canary there is neither module
nor exports
in package.json
. Also, I cannot see any ESM build under node_modules/next
.
So I can't quite see why typescript should resolve the package...
I might be wrong, but it'll need an ESM build and exports
field in package.json
for this to work.
{
"name": "next",
"version": "13.2.4-canary.5",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
"repository": "vercel/next.js",
"bugs": "https://github.com/vercel/next.js/issues",
"homepage": "https://nextjs.org"
// ...
}
Take a look at this comment, if you haven't read #46676 completely yet:
- https://github.com/vercel/next.js/issues/46676#issuecomment-1451731653
Are The Types Wrong shows that the
next
package is not bundled properly to be consumed by node16 moduleResolution in an ESM project.
The next
package has incorrect types for the new TypeScript module formats.
The next package has incorrect types for the new TypeScript module formats.
Yep. I just wonder if this isn't the second problem needs solving; the first one being "Cannot find module"?
Replacing next/some-entrypoint
with next/some-entrypoint.js
works, but it’d be great to avoid it. If next
adds exports
into package.json
, it will be possible to use next/some-entrypoint
consistently. You can find an example of a workaround (next/some-entrypoint.js
) in 🐸 https://github.com/kachkaev/njt/pull/186.
We can get inspiration from @vercel/analytics/package.json, which contains exports
and works fine in moduleResolution=nodenext
+ type=module
. I did not have to replace @vercel/analytics/react
with @vercel/analytics/react
in the above PR.
I had a similar problem with Hono, but I solved it by changing the types directory to CJS.
https://github.com/honojs/hono/pull/747
Replacing
next/some-entrypoint
withnext/some-entrypoint.js
works, but it’d be great to avoid it
I've been using these imports with .js
suffixes (eg. next/image.js
, next/server.js
, next/link.js
, etc), and it's worked so far.
However, one big footgun with this which I just ran into (pretty hard to debug) is if you import next/link.js
, the feature of TypeScript type checking on Link[href]
using typedRoutes: true
by @shuding just fails silently:
import Link from 'next/link.js'; // ✅ import works with ESM + Node16 module resolution
<Link href="/unknown"> // ❌ href silently not checked, because `next/link` module in generated type declarations in .next/types/link.d.ts
The offending line in the .next/types/link.d.ts
file looks like this:
declare module 'next/link' {
Submitted a pull request.
Until that is merged/released, here is a local patch that you can use:
diff --git a/package.json b/package.json
index bfa500ed3b8526117c602e6ee9cc2aef847c317e..9025f24c698f89f231fd47da901d9ebc2c2f29cd 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,92 @@
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
+ "exports": {
+ ".": {
+ "import": "./index.js",
+ "types": "./index.d.ts"
+ },
+ "./amp": {
+ "import": "./amp.js",
+ "types": "./amp.d.ts"
+ },
+ "./app": {
+ "import": "./app.js",
+ "types": "./app.d.ts"
+ },
+ "./babel": {
+ "import": "./babel.js",
+ "types": "./babel.d.ts"
+ },
+ "./cache": {
+ "import": "./cache.js",
+ "types": "./cache.d.ts"
+ },
+ "./client": {
+ "import": "./client.js",
+ "types": "./client.d.ts"
+ },
+ "./config": {
+ "import": "./config.js",
+ "types": "./config.d.ts"
+ },
+ "./document": {
+ "import": "./document.js",
+ "types": "./document.d.ts"
+ },
+ "./error": {
+ "import": "./error.js",
+ "types": "./error.d.ts"
+ },
+ "./font": {
+ "import": "./font/index.js",
+ "types": "./font/index.d.ts"
+ },
+ "./font/google": {
+ "import": "./font/google.js",
+ "types": "./font/google.d.ts"
+ },
+ "./font/local": {
+ "import": "./font/local.js",
+ "types": "./font/local.d.ts"
+ },
+ "./head": {
+ "import": "./head.js",
+ "types": "./head.d.ts"
+ },
+ "./headers": {
+ "import": "./headers.js",
+ "types": "./headers.d.ts"
+ },
+ "./image": {
+ "import": "./image.js",
+ "types": "./image.d.ts"
+ },
+ "./link": {
+ "require": "./link.js",
+ "types": "./link.d.ts"
+ },
+ "./navigation": {
+ "import": "./navigation.js",
+ "types": "./navigation.d.ts"
+ },
+ "./router": {
+ "import": "./router.js",
+ "types": "./router.d.ts"
+ },
+ "./script": {
+ "import": "./script.js",
+ "types": "./script.d.ts"
+ },
+ "./server": {
+ "import": "./server.js",
+ "types": "./server.d.ts"
+ },
+ "./web-vitals": {
+ "import": "./web-vitals.js",
+ "types": "./web-vitals.d.ts"
+ }
+ },
"peerDependenciesMeta": {
"node-sass": {
"optional": true
Since nextjs itself is a js bundler, the preferred moduleResolution should be bundler
IMO. That would solve all the problem, however nextjs does not support bundler and would overwrite it back to node
...
ref: https://github.com/vercel/next.js/pull/50289
FYI, this went away for me when I changed these settings in my tsconfig.json
:
-
module
:nodenext
->esnext
-
moduleResolution
:nodenext
->bundler
(Read more here: https://www.totaltypescript.com/tsconfig-cheat-sheet)
Note that nodenext
and bundler
are fundamentally different module resolution algorithms.
With this context, we’re ready to begin answering your question directly. The biggest, most noticeable difference between
--module nodenext
and--module esnext
is that the former implies--moduleResolution nodenext
, a new resolution mode designed for Node’s specific implementation of co-existing ESM and CJS, while the latter does not imply amoduleResolution
setting because there is no such corresponding setting in TypeScript right now. Put another way, when you say you’re using--module esnext
, you’re allowed to write, and we will emit, the latest and greatest ES module code constructs, but we will not do anything differently with deciding how imports resolve. You’ll likely continue using--moduleResolution node
, which was designed for Node’s implementation of CJS. What does this mean for you? If you’re writing ESM for Node, you can probably make some stuff work with--module esnext
and--moduleResolution node
, but newer Node-specific features like package.json exports won’t work, and it will be extremely easy to shoot yourself in the foot when writing import paths. Paths will be evaluated by tsc under Node’s CJS rules, but then at runtime, Node will evaluate them under its ESM rules since you’re emitting ESM. There are significant differences between these algorithms—notably, the latter requires relative imports to use file extensions instead of dropping the.js
, andindex
files have no special meaning, so you can’t import the index file just by naming the path to the directory.
– https://stackoverflow.com/a/71473145/368691
nodenext
needs to be supported by Next.js. Otherwise this causes major maintenance headache for organizations with large monorepos.
As an alternative to @lucgagan suggestion above, adding wildcard exports to package.json
works for me:
{
...
"exports": {
".": "./index.js",
"./*": "./*.js",
"./font/*": "./font/*/index.js",
"./dist/*": "./dist/*/index.js"
}
}
Node's support for subpath patterns is documented here.
EDIT: sorry, just realized this is similar to this comment
In nodenext
we have:
ts: 'Link' cannot be used as a JSX component.
The workaround is:
import Link from "next/link.js";
<Link.default href="" />
However, this does not work (still needs Link.default
):
import { default as Link } from "next/link.js";
<Link href="" />
Adding
+ export { Link }
export default Link
to next/link.d.ts
works:
import { Link } from "next/link.js";
<Link href="" />
I guess the wildcard export is not working:
export * from './dist/client/link'
import * as Link from 'next/link' // only contains `Link.default`
FYI, this went away for me when I changed these settings in my
tsconfig.json
:
module
:nodenext
->esnext
moduleResolution
:nodenext
->bundler
(Read more here: https://www.totaltypescript.com/tsconfig-cheat-sheet)
You missed the point. OP is trying to have a working ESM+Typescript setup so he needs NodeNext in both.
@lucgagan and @parcelgraph your solutions might be a start but it is not enough. The project needs to create correct .d.ts build outputs as well. For example:
Userland code:
import Document from 'next/document';
will not work even with your export changes, since the build output from next is CJS. For ESM+TS to work the dist folder need to contain .mjs
files containing code employing import/export
syntax (no require
!), and .d.mts
files.
Today the build output only contains CJS format, i.e. .js
files employing module.exports/require
syntax, and .d.ts
files. This type of output only works for non-ESM projects.
Solution
The complete solution to this problem summarized in a single sentence is to emit both CJS and ESM build outputs in the published artefact, and point the package.json to the correct locations, so the artefact is 100% backwards compatible with existing CJS projects while still working correctly with new ESM projects.
In other words, instead of outputting a single build into dist/*
maintainers should ALSO export a ESM build to e.g. dist/esm/*
(in addition to the existing CJS build). The contemporary tool for this is esbuild
, it can be fine to use directly but there are also high level tools for making this easier, such as tsup to help do maintainers control this step and make it more maintainable through configuration.
So anyway, when the ESM+CJS build step is working, the package.json should point to the respective output folders:
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.mjs", // ./dist/esm/index.d.mts will be found automatically
"require": "./dist/index.js"
},
"./document": {
"import": "./dist/esm/document.mjs", // ./dist/esm/document.d.mts will be found automatically
"require": "./dist/document.js"
},
... // etc
-
main
points to CJS artefact -
types
points to CJS type defs -
exports
explicitly points users where to find the file using deep imports e.gimport Document from 'next/document'
Then it will work* and everyone will be happy.
*) To reiterate, the ESM build output must use full path import syntax e.g.
import Gateway from './gateway/gateway.mjs';
import { Params } from './types.mjs';
and that's why esbuild
and/or tsup
is so important since tsc
cannot write this type of output afaik.
@klippx thank you for your wonderful explanation. We terminated work on our CJS -> ESM conversion after only a few hours, instead of spending days on trying to fix our already too complicated setup.
I'm interpreting this as that next.js isn't ready for TS ESM, and that it won't be until the above things are fixed.