TypeScript

Fix TS2688: node_modules Is the Wrong typeRoots

Reproduce the broad typeRoots mistake, distinguish it from missing @types packages, and preserve custom global declarations while repairing tsconfig.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

If TS2688 names several ordinary packages from node_modules, inspect compilerOptions.typeRoots. Putting the entire dependency directory there makes its package folders candidates for implicit type libraries when automatic enumeration is enabled. Remove that broad root; retain the actual type-package directories. The original failure reproduces by default on TypeScript 5.9.3, while TypeScript 6.0.3 and 7.0.2 require the wildcard types setting to enable the same enumeration.

The source question already tried excluding dependencies and enabling skipLibCheck. Its significant configuration was:

json
{
  "compilerOptions": {
    "typeRoots": ["./node_modules", "./node_modules/@types"]
  }
}

The first directory is the mistake. A type root is not a general search path for every JavaScript package your application might import.

Why a runtime package becomes an implicit type library#

The typeRoots reference describes directories containing type packages. When enumeration runs, TypeScript walks those roots and collects package names. The TypeScript 5.9.3 resolver implements that walk in getAutomaticTypeDirectiveNames. It does not require an application import before considering each collected package.

Consequently, a JavaScript-only package can be perfectly valid at runtime and still become an invalid implicit type-library entry. You told the compiler to search its parent directory as a collection of declarations. Installing a declaration package for every name in the error list would address symptoms of that unnecessary search rather than fixing its scope.

This differs from a broken directory under the correct root. If your root already points only to the types directory and the error names an incomplete package there, use the body-parser implicit-type-library diagnosis. That page covers missing declaration entries and leftovers inside the types collection; this case starts one directory too high.

A minimal failure with no application import#

The reproduction ran native compilers on Windows with Node.js 26.7.0, using TypeScript 5.9.3, 6.0.3 and 7.0.2. It used a hand-authored fixture package called runtime-only, not a package installed from the registry. Its directory contained a package manifest naming an ordinary JavaScript entry point and an index.js file, with no declarations. The only application file was:

typescript
export const status = "ready";

Nothing imports the fixture package. With the broad root and the default types behaviour, TypeScript 5.9.3 nevertheless printed:

text
error TS2688: Cannot find type definition file for 'runtime-only'.
  The file is in the program because:
    Entry point for implicit type library 'runtime-only'

Changing only the root list to the types directory made the same fixture pass. Leaving the broad list unchanged and adding both an exclusion for dependencies and the declaration-checking shortcut did not help.

The top-level exclude reference limits what source discovery through include adds. It is not a prohibition on all compiler access to dependencies. Meanwhile, skipLibCheck skips checking declaration files; it cannot provide a declaration entry point that failed to resolve. The negative control therefore matches the documented jobs of those options.

Repair the configuration that your build actually uses#

1. Identify the compiler and effective options#

From the package that owns the failing build, with its TypeScript dependency installed:

bash
npx tsc --version
npx tsc -p tsconfig.json --showConfig

If the build selects another config, use that file after the project option. Look for both root and types settings in the output. The compiler CLI reference documents printing the final configuration. It also explains that passing source filenames directly bypasses tsconfig processing, so compiling one source file by hand is not an equivalent check of this problem.

Inherited paths need attention too. Under extends, a relative path is resolved from the configuration file where it originated. A root supplied by a shared config in another directory may not refer to the folder you expected. Inspect the effective setting before adding another path as a guess.

2. Remove the ordinary dependency directory from the roots#

Edit the existing root list, preserving the rest of your application's settings. For the simple fixture, the corrected complete configuration is:

json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "typeRoots": ["./node_modules/@types"]
  },
  "include": ["src"]
}

The compiler settings other than the root list were identical in the failing and passing fixtures. This is not an instruction to replace an existing React or framework configuration wholesale. Keep any genuine custom type-package root alongside the standard one. You can also omit the root setting when you have no custom restriction, but that is a separate choice from removing the single incorrect entry.

3. Choose global type packages deliberately#

Do not copy an empty roots array merely because an answer reports success. It can stop the errors by preventing discovery of declarations your program needs. In a second reproduction, a custom declaration package supplied a global called PROJECT_STAGE. Replacing its root with an empty array removed that global and produced:

text
src/index.ts(1,22): error TS2304: Cannot find name 'PROJECT_STAGE'.

That happened on all three tested compilers with enumeration enabled. A configuration that explicitly selected the custom package and retained its directory passed on each compiler. For a project containing local-types/project-env/index.d.ts, the tested arrangement was:

json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "typeRoots": ["./local-types"],
    "types": ["project-env"]
  },
  "include": ["src"]
}

The declaration file contained:

typescript
declare const PROJECT_STAGE: "test";

The application read that declaration with:

typescript
export const stage = PROJECT_STAGE;

Use your actual package names and directories. If changing global type selection exposes a missing environment name inside axios declarations, the AbortSignal type-loading fix explains the separate Node-versus-browser library decision.

Verify the root fix across compiler generations#

The TypeScript 6.0 release notes explain that types now defaults to an empty list. A wildcard entry restores the previous automatic enumeration. The native fixture gave these results:

Root selection and types setting5.9.36.0.37.0.2
Broad roots, types omittedTS2688PassPass
Broad roots, enumeration enabledTS2688TS2688TS2688
Only the types root, enumeration enabledPassPassPass
Broad roots, explicit empty typesPassPassPass

Enumeration enabled means the old default for the first compiler and the wildcard list for the latter two. A newer compiler passing with its empty default does not prove that your broad root was appropriate; it may simply never walk it. Check inherited options if a current project still reports implicit-library errors.

After the repair, run the same project compilation that failed:

bash
npx tsc -p tsconfig.json --noEmit

If the error changes to invalid declaration syntax, inspect compiler compatibility instead of widening roots again: TS1084 in older compilers is one concrete example. If a real application import now reports a missing declaration file, that is the separate TS7016 module-typing problem. Keep implicit global-type discovery and imported-module resolution separate, and change the setting responsible for the diagnostic you actually have.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.