DEV Community

Cover image for Force tsc to Ignore node_modules: Fix TS Errors
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Force tsc to Ignore node_modules: Fix TS Errors

The error that won't go away

I ran tsc --noEmit on a project that compiled fine in my editor, and the terminal lit up with a dozen type errors — all from inside node_modules. The exact output looked something like this:

node_modules/some-library/dist/index.d.ts:45:18 - error TS2314: Generic type 'Array<T>' requires 1 type argument(s).

node_modules/another-package/types.d.ts:12:5 - error TS2430: Interface 'Options' incorrectly extends interface 'Config'.
Enter fullscreen mode Exit fullscreen mode

The project built and ran without issues. My own source code had zero errors. But tsc insisted on crawling through every .d.ts file in node_modules and reporting problems that weren't mine to fix. I had already added "exclude": ["node_modules"] to tsconfig.json, and it did nothing.

The fix is a single compiler option most projects should already have enabled: "skipLibCheck": true. If that alone doesn't silence every false positive, there are two more precise settings that close the remaining gaps.

Why exclude doesn't stop tsc from checking dependencies

The exclude array in tsconfig.json controls which files TypeScript considers part of your program. It prevents tsc from discovering .ts and .tsx files in those directories as standalone compilation units. But it does not stop type resolution.

When your code contains import { something } from "a-library", TypeScript follows the module resolution algorithm to find the package's entry point — typically the types or typings field in its package.json, or an index.d.ts file. Once resolved, that declaration file becomes part of the compilation graph. exclude cannot sever that link because the file is now a dependency of your source code, not an independently discovered file.

The relevant code path in a typical tsconfig.json looks like this:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

That exclude array prevents tsc from walking node_modules as a root directory. But the moment any file in src imports from a package, tsc resolves the package's declaration files and type-checks them. The errors you see are real type violations inside those .d.ts files — they just aren't your responsibility to fix.

The fix: skipLibCheck and two backup options

The primary fix is adding "skipLibCheck": true to compilerOptions. This tells TypeScript to skip type-checking all declaration files (.d.ts), regardless of where they live. It doesn't affect type-checking of your own .ts and .tsx files.

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

That single line eliminates every error originating from a .d.ts file. Your own source code still gets full strict checking. The trade-off is that tsc won't catch genuine mismatches between a library's declared types and its runtime behavior — but in practice, those mismatches surface as runtime errors, not build failures, and skipLibCheck is enabled by default in every starter template from Next.js, Vite, and Create React App for exactly this reason.

When skipLibCheck isn't enough

I've hit two scenarios where skipLibCheck alone didn't silence every node_modules error.

Scenario 1: A package ships raw .ts files instead of compiled .d.ts files. Some packages include their source TypeScript in the published tarball, and their package.json points types at a .ts file. skipLibCheck only skips .d.ts files, so tsc will still type-check those raw .ts files. The fix is to add the specific package path to exclude:

{
  "compilerOptions": {
    "skipLibCheck": true
  },
  "exclude": ["node_modules", "node_modules/problematic-package/**/*.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Scenario 2: A monorepo with hoisted dependencies. In a pnpm or Yarn workspaces setup, node_modules might contain symlinks to packages that live outside the project root. TypeScript can follow those symlinks and discover .ts files that exclude patterns don't match. The fix here is to set "rootDir" explicitly so tsc knows where your program boundaries are:

{
  "compilerOptions": {
    "skipLibCheck": true,
    "rootDir": "src"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

Step by step

  1. Open tsconfig.json at the project root.
  2. Locate the compilerOptions block.
  3. Add "skipLibCheck": true inside it.
  4. If you're in a monorepo, also set "rootDir": "src" (or wherever your source lives).
  5. Save and run tsc --noEmit again.

Verify the fix

Run the same command that produced the errors:

npx tsc --noEmit
Enter fullscreen mode Exit fullscreen mode

Expected output:

# No output — clean exit code 0
Enter fullscreen mode Exit fullscreen mode

If you were seeing errors from a specific package and they're gone, the fix worked. To confirm skipLibCheck is active without relying on the absence of errors, you can run tsc with the --listFiles flag and grep for .d.ts files from node_modules:

npx tsc --noEmit --listFiles | grep "node_modules.*\.d\.ts" | head -5
Enter fullscreen mode Exit fullscreen mode
# Still lists the files — they're part of the program, just not type-checked
node_modules/typescript/lib/lib.es2020.d.ts
node_modules/@types/react/index.d.ts
Enter fullscreen mode Exit fullscreen mode

The files still appear because they're still resolved and used for type information. They just aren't checked for internal consistency. That's the behavior you want.

Two variants that still trip people up

Variant A: Errors from @types/* packages. skipLibCheck covers these because they ship .d.ts files. If you're still seeing errors from @types/react or similar, confirm skipLibCheck is spelled correctly and inside compilerOptions — I've seen it accidentally placed at the top level of tsconfig.json, where it has no effect.

Variant B: Errors from a package that uses export = syntax. Some older packages declare types with export = instead of ES module syntax. If esModuleInterop is off, tsc may report import-style errors even with skipLibCheck enabled because the error is about how your code imports the package, not about the declaration file itself. The fix for this specific case is enabling esModuleInterop:

{
  "compilerOptions": {
    "skipLibCheck": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a separate class of error — it's a module-resolution mismatch, not a type-checking error inside the dependency. I cover the import-side of this problem in Fix TS2305: Module Has No Exported Member in TypeScript.

Why this keeps happening across projects

The root cause is that TypeScript's default behavior prioritizes soundness over pragmatism. When strict mode is on, tsc applies the same rigorous checks to every file in the compilation graph — including third-party declarations you didn't write and can't fix. Library authors ship .d.ts files with varying levels of strictness compliance, and a single generic constraint violation in one package can cascade into dozens of errors in your build.

The invariant to remember: exclude controls file discovery, skipLibCheck controls file checking. They solve different problems. If you're setting up a new project, enable skipLibCheck from day one. Every production TypeScript codebase I work on has it enabled, and I've never regretted it.

For projects where you want an extra safety net, you can combine skipLibCheck with a CI step that runs tsc without it on a schedule — not on every commit, but weekly — to catch regressions in your own type declarations. That way you get fast builds and still have visibility into dependency type health.

If you're dealing with declaration-file errors in your own code rather than in dependencies, the diagnosis is different. I walk through that in Fix TS7016: Could Not Find Declaration File for Module. And if you've set up path aliases that tsc isn't resolving, the fix is in Fix tsconfig Paths Not Working in Next.js.

FAQ

Does skipLibCheck affect type safety in my own code? No. It only skips checking .d.ts files. Your .ts and .tsx files still get full type-checking with all the strictness settings you've configured. The types from dependencies are still used for inference and error detection in your code — they just aren't validated for internal consistency.

Why does tsc check node_modules even when I use "exclude"? Because exclude only prevents TypeScript from discovering files as top-level compilation units. When your code imports a package, TypeScript resolves that package's type declarations and includes them in the compilation graph. The only way to stop type-checking those resolved declaration files is skipLibCheck.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)