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'.
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"]
}
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"]
}
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"]
}
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"]
}
Step by step
- Open
tsconfig.jsonat the project root. - Locate the
compilerOptionsblock. - Add
"skipLibCheck": trueinside it. - If you're in a monorepo, also set
"rootDir": "src"(or wherever your source lives). - Save and run
tsc --noEmitagain.
Verify the fix
Run the same command that produced the errors:
npx tsc --noEmit
Expected output:
# No output — clean exit code 0
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
# 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
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
}
}
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
- Fix TS7016: Could Not Find Declaration File for Module
- Fix TS2305: Module Has No Exported Member in TypeScript
- Fix tsconfig Paths Not Working in Next.js
- TS2564: Why Constructor Methods Don't Count as Init
Originally published at https://www.iloveblogs.blog
Top comments (0)