TypeScript

Module resolves to an untyped module: fix TS2665 in .d.ts

Why a hand-written declaration for an untyped package fails with "cannot be augmented", why the same block works in a .d.ts but not a .ts file under module node16 to nodenext, and the one-line move that fixes it.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

You wrote a declare module 'node-helper-lib' block for a JavaScript package that has no types, and the compiler rejects it with TS2665. The block itself is fine. What changed its meaning is something at the top level of the same file: an import, an export {}, or the file being a .ts file when module is any of node16, node18, node20 or nodenext. Any of those makes the file a module, and inside a module declare module is a module augmentation, which the compiler refuses for an untyped module. Move the imports inside the declare module block, delete stray top-level exports, and keep the declaration in a .d.ts file.

The error#

The Stack Overflow question behind this page (TypeScript 2.1.4, 141 votes) put a hand-written declaration in a typings folder and got this in Visual Studio Code, underlined on the module name:

text
[ts] Invalid module name in augmentation. Module 'node-helper-lib'
resolves to an untyped module at '{project
path}\node_modules\node-helper-lib\index.js', which cannot be
augmented.

The question shortened the first lines of its .d.ts file to declare....(something else), so the top of the file is not visible. The fix with the most votes (moving imports inside the block) only makes sense if those hidden lines held a top-level import, and that is the case reproduced below.

The message has not changed since. It is diagnostic 2665 in the compiler's diagnosticMessages.json, and it is still present in the Go-based compiler. A minimal project reproduces it on TypeScript 7.0.2 and 5.9.3, in the official node:24-bookworm-slim image with @types/node 24.19.2, on 24 October 2026. node-helper-lib is the name of the reader's package in the question, not something to install from npm. In the reproduction it is a local stub written by hand under node_modules/: a package.json with main: index.js and an index.js that assigns a class to module.exports, with no types anywhere. Use your own package's name wherever it appears below. The declaration file starts with an import, as declaration files often do:

typescript
// typings/node-helper-lib.d.ts
import { EventEmitter } from 'node:events';
 
declare module 'node-helper-lib' {
  class Helper extends EventEmitter {
    constructor(opts: { name: string });
    greet(): string;
  }
  export = Helper;
}

tsc -p . on TypeScript 7.0.2 prints:

text
src/index.ts(1,20): error TS7016: Could not find a declaration file for module 'node-helper-lib'. '/work/1-toplevel-import/node_modules/node-helper-lib/index.js' implicitly has an 'any' type.
typings/node-helper-lib.d.ts(3,16): error TS2665: Invalid module name in augmentation. Module 'node-helper-lib' resolves to an untyped module at '/work/1-toplevel-import/node_modules/node-helper-lib/index.js', which cannot be augmented.
exit code: 1

TypeScript 5.9.3 prints the same two errors and exits with code 2. Note the pair. The TS7016 on the import line is the error the declaration was written to fix in the first place. It is still there because the rejected augmentation declared nothing. The TS7016 page covers the first step (installing @types, checking for bundled types, writing a declaration). This page is about the declaration you already wrote being refused.

Why it happens#

The same syntax means two things. The TypeScript modules reference puts it this way: "This module declaration syntax becomes a module augmentation when the file is a module, meaning it has a top-level import or export statement". In a script file (no top-level import or export), declare module 'node-helper-lib' { … } is an ambient module declaration. It creates the module's types from nothing, which is exactly what an untyped package needs.

In a module file, the same block is a module augmentation. An augmentation does not create the module's types from nothing: the compiler first resolves the name the way it resolves an import, and the TS2665 message shows that step, "Module 'node-helper-lib' resolves to an untyped module at …". When that lookup lands on a plain index.js with no declaration file, the same message states the rule: an untyped module "cannot be augmented". That is the whole of TS2665: the block is in augmentation mode, and its target has no types.

So the single import { EventEmitter } from 'node:events' on line 1 is the cause. It is there for a good reason, because the class extends EventEmitter. But it changes the file from a script into a module.

The accepted answer on the question gives a different reason: that augmentation means "declaring a module in the same file with other module declaration(s)". That reading does not hold up. As a negative control, a .d.ts file with no top-level import, holding two declare module blocks for two different untyped packages, compiled cleanly:

text
### scenario=3-two-blocks-script tsc=Version 7.0.2
exit code: 0

TypeScript 5.9.3 gave the same exit code 0. How many blocks the file holds does not matter. What matters is whether the file is a module.

Fix#

  1. Move every import inside the declare module block. The modules reference describes this exact pattern: "Ambient modules may use imports inside the module declaration body to refer to other modules without turning the containing file into a module". It is also the higher-voted answer on the question.

    typescript
    // typings/node-helper-lib.d.ts
    declare module 'node-helper-lib' {
      import { EventEmitter } from 'node:events';
     
      class Helper extends EventEmitter {
        constructor(opts: { name: string });
        greet(): string;
      }
      export = Helper;
    }
  2. Remove any other top-level import or export from that file, including an export {} that an editor template or a lint rule may have added. On its own, that empty export at the end of the file reproduces TS2665 on both compiler versions:

    text
    typings/node-helper-lib.d.ts(1,16): error TS2665: Invalid module name in augmentation. Module 'node-helper-lib' resolves to an untyped module at '/work/7-export-empty/node_modules/node-helper-lib/index.js', which cannot be augmented.
  3. Keep the declaration in a .d.ts file, not a .ts file (the section below explains why this matters under nodenext), and make sure the folder is in the include list of tsconfig.json:

    json
    {
      "compilerOptions": {
        "target": "es2022",
        "module": "nodenext",
        "strict": true,
        "noEmit": true,
        "types": ["node"]
      },
      "include": ["src", "typings"]
    }

    The types entry is only there because the declaration imports from node:events. Since TypeScript 6.0 the default for types is an empty list, so @types/node is no longer loaded automatically. The TS2688 body-parser page explains that change.

With the import moved inside, tsc -p . exits with code 0 on both TypeScript 7.0.2 and 5.9.3, and the TS7016 on the import line is gone too.

Verify the fix#

A clean exit code does not prove the types are real. A shorthand declaration would also clear TS7016 by making everything any. To check, use the class in a way only real types can judge. In the reproduction, line 1 of src/index.ts is the default import of the package taken from the question, and line 2 is blank. Lines 3 to 6 follow, with one call that should type-check and one that should not:

typescript
const h = new Helper({ name: 'London' });
console.log(h.greet());
h.on('ready', () => console.log('ready'));
h.greet(42);

Both compilers report exactly one error:

text
src/index.ts(6,9): error TS2554: Expected 0 arguments, but got 1.

h.on type-checks because it is inherited from the imported EventEmitter, which shows the inner import resolved. h.greet(42) is rejected because greet was declared with no parameters, which shows the class is not any. Delete the deliberate mistake, and the declaration is done.

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.

The .ts file that is always a module under node16 to nodenext#

The third answer on the question says the problem went away after renaming the declaration file from .ts to .d.ts. When module is any of node16, node18, node20 or nodenext, that rename is not cosmetic. The reproduction used a .ts file with no import and no export at all, just the declare module block:

text
typings/node-helper-lib.ts(1,16): error TS2665: Invalid module name in augmentation. Module 'node-helper-lib' resolves to an untyped module at '/work/5-ts-file-script/node_modules/node-helper-lib/index.js', which cannot be augmented.
exit code: 1

TypeScript decides whether a file is a script or a module through the moduleDetection option. The tsconfig reference lists "auto" as the default, and describes "force" as: "Ensures that every non-declaration file is treated as a module." But the compiler only uses "auto" as the fallback. When moduleDetection is not set and module is any of node16, node18, node20 or nodenext, the computed value is force (the code tests the range from Node16 to NodeNext, which holds those four values). That is the code in utilities.ts at v5.9.3, and the Go port does the same in compileroptions.go at v7.0.2. So under any of those four settings every .ts file is a module, and any declare module block in it is an augmentation. Declaration files are excluded from force, which is why the .d.ts rename works.

Setting moduleDetection to auto explicitly confirms the mechanism: the same .ts file then compiled with exit code 0 on both versions. Do not use that as the fix, though. It changes how every file in the project is classified. Renaming one file to .d.ts changes only that file.

When you really do want an augmentation#

The reverse situation uses the same syntax. If the package already ships types, or has an @types package, and you want to add a method or a property to it, then the top-level import is correct. The file must be a module so that the block patches the existing declarations. In that case TS2665 never appears, because the name resolves to a typed module. If it does appear while you are trying to augment, the types you meant to extend are not being resolved, and that is a TS7016 problem to solve first.

One more error tends to follow once the declaration loads. The question's index.ts default-imports the package (import Helper) while the declaration uses export = Helper. Under nodenext that interop is allowed. With older module settings and esModuleInterop off, the compiler can instead refuse the default import of an export = module. That case is Cause C of the TS2305 page, which covers esModuleInterop, import x = require() and the CommonJS interop errors.

So when TS2665 appears in a declaration file you wrote yourself, start by looking at the top of the file rather than at the block the error underlines. If the file has a top-level import or export, or it is a .ts file in a project whose module is any of node16, node18, node20 or nodenext, it is a module, and its declare module block is an augmentation. Move the imports inside the block and keep the file a .d.ts, and the same block declares the package instead.

Further reading: Fix TS2882: side-effect import of './globals.css'.

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