What this usually means
Most declaration file errors fall into one of three categories: (1) the .d.ts file doesn't exist for a JavaScript library you're using, (2) your custom .d.ts file has a syntax error or mismatched types, or (3) there are conflicting declarations from multiple sources (e.g., two @types packages defining the same global type). The TypeScript compiler uses declaration files to understand the shape of JavaScript modules; when it can't find one or finds an incorrect one, it throws these errors. The key is to look at the exact error code and the file path mentioned.
The first ten minutes — establish facts before touching code.
- 1Run `tsc --noEmit` to get the full list of declaration errors with file paths.
- 2Check if the module has types via `npm show @types/<module> versions` or look for a `types` field in its package.json.
- 3For custom .d.ts files, open the file and check for missing `export` or incorrect `declare module` syntax.
- 4Search your project for duplicate type names: `grep -r 'interface MyType' src/`
- 5Inspect tsconfig.json for `typeRoots` and `types` settings that might exclude needed declarations.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchError output from `tsc --noEmit` — exact file and line number
- searchThe @types package version in node_modules/@types/<module>/index.d.ts
- searchYour custom declaration files (e.g., src/types/globals.d.ts)
- searchtsconfig.json's `include`, `exclude`, `files`, `typeRoots`, `types` fields
- searchThe module's package.json — check `types` or `typings` field
- searchGlobal type declarations in node_modules/@types/**/globals.d.ts
- searchTriple-slash directives in your .ts files: `/// <reference types="..." />`
Practical causes, not theory. These are the things you will actually find.
- warningMissing @types package for a third-party JS library (e.g., `lodash` needs `@types/lodash`)
- warningIncorrect `declare module` syntax — using `declare module 'foo'` instead of `declare module 'foo' { ... }`
- warningConflicting global declarations from two different @types packages (e.g., both define `EventEmitter`)
- warningCustom .d.ts file not included in compilation (tsconfig.json `include` misses it)
- warningUsing `export =` in a module that expects ES modules (or vice versa)
- warningOutdated @types that mismatch the actual library version
Concrete fix directions. Pick the one that matches your root cause.
- buildInstall missing types: `npm install --save-dev @types/<module>` (or `yarn add --dev @types/<module>`)
- buildCreate a custom .d.ts file with proper module augmentation: `declare module 'untyped-module' { export function foo(): void; }`
- buildUse `declare global { ... }` inside a module to extend global types without conflict
- buildAdd the custom .d.ts path to tsconfig.json's `include` or `files` array
- buildSet `skipLibCheck: true` in tsconfig.json to bypass type checking in node_modules (use as temporary workaround)
- buildExplicitly import types instead of relying on global declarations: `import { SomeType } from 'some-module'`
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `tsc --noEmit` again — the error count should drop to zero
- verifiedOpen a file that uses the type and confirm IntelliSense shows the correct definition
- verifiedCheck that `npm ls @types/<module>` shows the expected version
- verifiedFor custom declarations, verify the module's exports by importing and using them in a .ts file
- verifiedIn a CI pipeline, ensure the build passes with the same tsconfig.json
Things that make this bug worse or harder to find.
- warningUsing `declare module 'foo'` without a body (creates an empty module, breaking imports)
- warningEditing .d.ts files inside node_modules — changes are lost on `npm install`
- warningSetting `typeRoots` too narrowly, excluding necessary @types packages
- warningIgnoring `skipLibCheck` — it hides real errors in your own declaration files
- warningUsing `any` casts to silence the error without understanding the root cause
The Silent .d.ts Conflict That Broke CI
Timeline
- 09:15CI build fails with TS2403: Subsequent variable declarations must have the same type. Error points to node_modules/@types/events/index.d.ts and node_modules/@types/node/index.d.ts both declaring 'EventEmitter'.
- 09:20Check recent commits — someone added `@types/events` as a dependency for a new event emitter library.
- 09:25Run `npm ls @types/events` and see it's a transitive dependency pulled in by an older version of `eventemitter3`.
- 09:30Read the error: both packages export a global `EventEmitter` interface with slightly different signatures.
- 09:35Try `skipLibCheck: true` — build passes but IntelliSense is broken for those types.
- 09:40Decide to override the transitive dep: add `@types/events` as a devDependency at a version that aligns with `@types/node`.
- 09:45Update package.json, run `npm install`, build passes. CI green.
- 09:50Push fix, add a comment explaining the conflict. Schedule a tech debt ticket to upgrade eventemitter3.
The CI pipeline started failing mysteriously on a Monday morning. The error was TS2403: 'Subsequent variable declarations must have the same type.' pointing to two different @types packages. I hadn't seen that error before, but it was clear that two declaration files were trying to define the same global type.
I quickly found that someone had added `eventemitter3` which pulled in `@types/events` as a transitive dependency. That package declares a global `EventEmitter` interface, which conflicts with the one in `@types/node`. The signatures were slightly different, so TypeScript couldn't merge them.
The quick fix was to align the versions by adding `@types/events` as a direct devDependency at a compatible version. But the real lesson was to avoid relying on transitive type packages. We later upgraded eventemitter3 to a version that doesn't depend on @types/events, and we added a CI check for duplicate global declarations.
Root cause
Conflicting global `EventEmitter` declarations from `@types/events` (transitive) and `@types/node` (direct).
The fix
Added `@types/events` as a direct devDependency with a version compatible with `@types/node`.
The lesson
Always check transitive dependencies for type conflicts. Use `npm ls @types/*` to audit type packages, and consider setting `typeRoots` explicitly to avoid pulling in unwanted global declarations.
A common mistake is writing `declare module 'foo'` without a body. This makes the module exist but with no exports, causing errors when you try to import anything from it. Instead, you must provide the module's shape inside the declaration.
For example, to declare a module without types: `declare module 'foo' { export const bar: string; }`. If you need to add to an existing module, use module augmentation: `declare module 'foo' { export function newFunc(): void; }`. For global types, wrap them in `declare global { }` inside a module file (with at least one `import` or `export` to make it a module).
TypeScript follows a specific order: first checks the `types` field in package.json, then `typeRoots` in tsconfig.json (defaults to node_modules/@types), then `include` and `files`. If you have a custom .d.ts file that isn't being picked up, check if it's in a directory outside `include` or if your `typeRoots` is set to a specific folder that excludes it.
Use `tsc --traceResolution` to see exactly how TypeScript resolves each module. This will show you which .d.ts file it picks and why. For example, if you see 'Resolution for module 'foo' failed', it means no declaration file was found.
When two @types packages define the same global interface (like `EventEmitter`), TypeScript throws TS2403. To identify the culprits, search all .d.ts files in node_modules for the identifier: `grep -r 'interface EventEmitter' node_modules/@types/`. Then decide which one to keep.
Options: (1) Remove the conflicting package if it's not needed, (2) align versions so they merge correctly, (3) use `declare global` in your own file to override or merge, or (4) set `skipLibCheck: true` (but this hides all type errors in node_modules). The best long-term fix is to remove the unnecessary package.
A missing closing brace, an extra comma, or using `export default` incorrectly can cause cryptic errors. Always validate your .d.ts file by opening it in an editor with TypeScript language service—it will highlight syntax errors.
Another pitfall: mixing ES module syntax (`export default`) with CommonJS (`export =`). If the library uses `module.exports`, your .d.ts must use `export =` to match. For example: `declare module 'commonjs-module' { function foo(): void; export = foo; }`. Using `export default` here will cause TS1192: Module '...' has no default export.
Frequently asked questions
Why do I get 'Cannot find module' even though the .d.ts file exists?
The declaration file must be in a location that TypeScript searches. Check that the file is included via tsconfig.json's `include` or `files`, or that the module's package.json has a `types` field pointing to it. Also ensure the module name in `declare module` matches exactly what you import.
How do I write a declaration for a module that has no types?
Create a .d.ts file (e.g., `src/types/declarations.d.ts`) and write: `declare module 'untyped-module';`. This makes the module exist with implicit `any` types. For better safety, specify the exports: `declare module 'untyped-module' { export function myFunc(): void; }`.
What does TS2300 'Duplicate identifier' mean?
It means two declarations with the same name exist in the same scope. This often happens when you both `import` a type and globally declare it. Check for accidental global declarations in .d.ts files, or use `declare global` inside a module to avoid polluting the global scope.
Should I use `skipLibCheck: true` to fix declaration errors?
Only as a temporary workaround. It skips type checking of all .d.ts files, including your own custom ones, which can hide real issues. Use it to unblock your team, but always fix the underlying problem (e.g., missing types, conflicts) and remove the flag.
Can I edit .d.ts files in node_modules?
Technically yes, but changes are lost on `npm install`. Instead, create a local declaration file that overrides or augments the module. Use module augmentation: `declare module 'existing-module' { ... }` to add missing types without modifying node_modules.