skip to content

What does TypeScript's `declarationMap` compiler option add on top of `declaration`, and what must a published package contain for it to work in a consumer's editor?

level: middleimportance: should knowfreq 38%

answer

  1. go-to-definition lands in generated code
  2. a second map, not the JS one
  3. points back at the original .ts
  4. the sources must ship too
  5. emits .d.ts.map beside each .d.ts

basics

~20 s

declarationMap emits a .d.ts.map next to each .d.ts, mapping declarations back to the original TypeScript. Go-to-definition then lands in real source instead of the generated declaration, but only if the package ships the maps and the .ts files they point at.

solid answer

~40 s

`declaration` alone gives a consumer a `.d.ts`, and every editor navigation lands there — a wall of bodiless signatures. `"declarationMap": true` additionally emits a `.d.ts.map` beside each declaration, a source map whose `sources` point at the original `.ts` file. When the editor follows go-to-definition, it reads that map and opens the real implementation with the real line and column. It also makes cross-project rename work properly in a monorepo. Two conditions: it requires `declaration` (or `composite`) to be on, and the package must actually **publish** the `.d.ts.map` files *and* the `.ts` sources they reference — a `files` list that ships only `dist` breaks the second hop and the editor falls back to the declaration. Shipping sources costs install size, which is why many libraries enable it only for internal packages.

code

json · 7 lines
json
{
  "name": "@acme/client",
  "version": "1.0.0",
  "types": "./dist/index.d.ts",
  "main": "./dist/index.js",
  "files": ["dist", "src"]
}

go deeper

for a junior

Know that a .d.ts is generated code with no bodies, and that declarationMap is the option that lets an editor jump to the original TypeScript instead.

for a middle

Explain the two-hop lookup — declaration, then map, then source file on disk — and state that declarationMap requires declaration or composite and emits .d.ts.map files.

for a senior

Demonstrate that you verify against the packed tarball: know that the .ts sources must be published for navigation to work, and be able to diagnose the silent fallback into dist.

for a principal

Own the publishing tradeoff — source navigation for every consumer versus install size and exposing internal source — and set a standard where internal workspace packages always enable it while public packages decide deliberately.

## The problem it solves With `declaration` on, a consumer of your library gets `.d.ts` files. Those files are generated, they contain no implementations, and they are what the editor shows when someone presses go-to-definition on one of your exports. The reader sees: ```ts export declare function createClient(opts: Options): Client; ``` which answers *what* but never *how*. In a monorepo this is worse than annoying: jumping from app code into a workspace package lands you in `dist`, and edits made there are overwritten on the next build. ## What the option emits ```json { "compilerOptions": { "declaration": true, "declarationMap": true, "outDir": "dist" } } ``` For every `dist/client.d.ts` the compiler now also writes `dist/client.d.ts.map`. It is an ordinary source map: a JSON file with `version`, `file`, `sources`, `names` and `mappings`, where `sources` is a relative path back to `src/client.ts`. Nothing about the `.js` output changes — this is the declaration-side analogue of what `sourceMap` does for runtime code, and the two options are independent. ## The two-hop lookup, and where it breaks When the editor resolves a symbol it does this: 1. Resolve the import to the `.d.ts` (via `package.json` and module resolution). 2. See the `//# sourceMappingURL=client.d.ts.map` comment at the end of the declaration, load the map, and translate the position. 3. Open the file named in `sources` — the original `.ts`. Step 3 is a **real file open on disk**. If the `.ts` file is not present in the installed package, the hop fails and the editor silently leaves you in the `.d.ts`. That is the single most common reason a team turns on `declarationMap` and observes no difference: the map shipped, but the sources did not. So a package that wants working source navigation must publish three things: the `.d.ts`, the `.d.ts.map`, and the `.ts` sources. Practically that means the `files` array in `package.json` includes `src` as well as `dist`, or `.npmignore` does not exclude it. `declarationDir` is worth a thought too, because moving declarations to a different tree changes the relative path baked into `sources`; the compiler computes it correctly, but only if you rebuild after changing the layout. ## Prerequisites and interactions - `declarationMap` cannot be set without `declaration` or `composite`; the compiler rejects the configuration otherwise. - With `composite` on, `declaration` is implied, so a project-reference build usually only needs the `declarationMap` line added. - It affects editors and language-server features only. It has zero effect on type checking, on emitted JavaScript, or on runtime behaviour. - `sourceMap` (for `.js`) and `declarationMap` (for `.d.ts`) solve different halves: the first makes a debugger step through your TypeScript, the second makes an editor navigate to it. ## When to enable it Internal or workspace packages: almost always. Navigation and rename across package boundaries is the main ergonomic win of a TypeScript monorepo, and it is exactly what breaks without maps. Public packages: it depends on whether you are willing to ship sources. The maps themselves are small; the `src` tree is not, and every consumer downloads it. Some libraries publish sources deliberately as documentation; others accept that outside users navigate to declarations. A cheap way to verify: pack the tarball, install it into a scratch project, and try go-to-definition. If you land in `dist`, one of the three artifacts is missing.

  • A team enabled `declarationMap` but go-to-definition still lands in the `.d.ts`. What do you check first?
    Whether the installed package actually contains the `.ts` sources. The editor follows the `.d.ts.map` to the path in its `sources` field and opens that file from disk; if `files` or `.npmignore` shipped only `dist`, the hop dead-ends and it stays in the declaration. Check the packed tarball, not the local build output.
  • How does `declarationMap` differ from `sourceMap`?
    They map different outputs. `sourceMap` produces `.js.map` so a debugger or stack trace resolves runtime positions back to TypeScript. `declarationMap` produces `.d.ts.map` so the editor's go-to-definition resolves a declaration back to the source that generated it. Neither implies the other, and a library often wants both.
  • Why does this matter more in a monorepo than for a public package?
    Because cross-package navigation and rename are constant there. Without maps, jumping into a workspace dependency lands in generated `dist` output, where edits get overwritten on the next build. Sources are already on disk in a workspace, so the map costs nothing — unlike a published package, where enabling it means shipping `src` to every consumer.

saying these in an interview costs you the question

  • Thinks declarationMap replaces sourceMap for debugging
  • Assumes shipping the .d.ts.map alone is enough
  • Believes it changes the emitted JavaScript
  • Says it can be enabled without declaration or composite
  • Thinks it affects type checking or build correctness

context