skip to content

In a TypeScript repo using project references, go-to-definition on a symbol from another project lands in a generated `.d.ts` file instead of the original `.ts` source. Which compiler option addresses this, and how?

level: middleimportance: should knowfreq 42%

answer

  1. the jump stops at generated output
  2. emit breadcrumbs back to the source
  3. a companion map file per declaration
  4. set it where the code is produced
  5. sources must still be on disk

basics

~10 s

Enable declarationMap in the referenced project. It emits a .d.ts.map alongside each declaration file, mapping every declaration back to the source that produced it, so go-to-definition and rename follow through to the original .ts.

solid answer

~40 s

Set `"declarationMap": true` in the referenced project and rebuild it. Declaration emit alone produces a `.d.ts` that has no link back to where each member came from, so the editor can only take you to the generated file — read-only, stripped of implementation, and useless for renaming. `declarationMap` emits a companion `.d.ts.map` recording that mapping, and the language service uses it to redirect go-to-definition, find-all-references and rename to the original source location. The cost is small extra output, and the source must actually be present on disk for the mapping to resolve — which it always is inside a repo, and often is not for a published package unless the sources ship too. In a repo where people edit across package boundaries daily, `declarationMap` in the shared base config is close to mandatory.

code

json · 11 lines
json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "rootDir": "./src",
    "outDir": "./dist"
  },
  "include": ["src"]
}

go deeper

for a junior

Know that declarationMap emits a map alongside each .d.ts so the editor can jump to the real source instead of the generated declaration, and that it is set in the project that produces the declarations.

for a middle

Explain the mechanism — a .d.ts.map in source-map format linking declaration positions to source positions, consumed by the language service for go-to-definition, references and rename — and distinguish it cleanly from sourceMap.

for a senior

Treat it as a developer-experience decision: put it in the shared base so every package has it, understand why stale maps land you at the wrong line, and know that published packages need their sources shipped for it to help consumers.

for a principal

Own the wider ergonomics question of a referenced-project repo: what navigation, rename and incremental-feedback guarantees engineers get across package boundaries, and what build-output and publishing policy those guarantees require.

## Why the editor lands in a `.d.ts` Project references change what a consuming project sees. When `app` references `core`, imports of `core` do not resolve to `core/src/*.ts`; they resolve to `core`'s emitted declarations, `core/dist/*.d.ts`. That is the whole point — it is what lets the two projects be compiled separately and what makes up-to-date checking possible. The side effect is that the editor's idea of "where is this defined?" is, by default, the declaration file. A `.d.ts` is a flattened, implementation-free description: ```ts // core/dist/index.d.ts export declare function parseConfig(input: string): Config; ``` Jumping there is technically correct and practically useless. There is no body to read, the file is regenerated on every build so editing it is pointless, and a rename applied there cannot propagate to the real definition. ## What declarationMap emits `"declarationMap": true` makes the compiler emit a `.d.ts.map` file next to each `.d.ts`. It is a source map, in the same format used for JavaScript source maps, but mapping declaration positions to the positions in the original `.ts` that produced them. ``` core/dist/index.d.ts core/dist/index.d.ts.map core/dist/index.js ``` With that map in place, the language service does not stop at the declaration. It reads the map, finds that `parseConfig` on line 3 of `index.d.ts` came from line 42 of `src/config.ts`, and takes you there instead. The same redirection powers find-all-references and rename, so refactoring a symbol across package boundaries starts to behave the way it does inside a single project. ## What it needs to work Three conditions. The referenced project must have been **built** — the map is emitted output, so a project that has never run through `tsc` has nothing to map with. The map must be **current**; a stale `.d.ts.map` maps to line numbers that have since moved, which produces the mildly maddening symptom of landing in the right file at the wrong place. And the **original sources must exist** at the paths the map records. Inside a monorepo that is automatic. For a package installed from a registry, it only holds if the publisher shipped the `.ts` sources in the package, which many do not — one reason `declarationMap` matters far more for internal projects than for published ones. Because it depends on emitted output, `declarationMap` belongs in the **producing** project's config, not the consuming one. In a monorepo the practical placement is the shared base config that every package extends, so no package can forget it: ```json { "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true } } ``` ## The related editor behaviour TypeScript's language service can also work directly from a referenced project's source files where they are available, rather than routing everything through built output — the behaviour known as source-of-project-reference redirect. It is on by default and can be turned off with the `disableSourceOfProjectReferenceRedirect` compiler option. When it is active, editing an upstream package can surface downstream without a rebuild, which is a different mechanism from `declarationMap` but aimed at the same complaint: the editor should feel like one codebase even though the build is many projects. Knowing both exist is what separates a confident answer from a partial one. `declarationMap` is the durable, build-output-based answer that also works for consumers of published packages; the redirect is an editor-side convenience within a repo. Neither changes what `tsc` checks — both are about navigation and refactoring ergonomics. ## Related but distinct options Do not confuse `declarationMap` with `sourceMap`, which maps emitted **JavaScript** back to TypeScript so a debugger can show you real source at a breakpoint. They solve different problems for different audiences — one serves the editor at author time, the other the debugger at run time — and a repo that cares about developer experience typically turns on both. `inlineSourceMap` and `inlineSources` are further variants for the JavaScript side and have no bearing on declaration navigation. ## The short version Without `declarationMap`, cross-project navigation stops at generated declarations. With it, the compiler ships the breadcrumbs that let the editor complete the trip to source — and the cost is a few extra files in the output directory.

  • Should `declarationMap` be set in the consuming project or the referenced one?
    The referenced one. The map is emitted output, produced when that project's declarations are generated, so only the producing project's config can turn it on. In a monorepo the usual placement is the shared base config every package extends, so no package can silently omit it.
  • How does `declarationMap` differ from `sourceMap`?
    `sourceMap` maps emitted JavaScript back to TypeScript so a debugger can break on real source at run time. `declarationMap` maps emitted `.d.ts` back to TypeScript so the editor can navigate and rename at author time. Different consumers, different output files; a repo that cares about both turns on both.
  • Why does `declarationMap` often fail to help for a package installed from npm?
    The map records paths to the original `.ts` files, and resolving it requires those files to exist. Many published packages ship only `.js` and `.d.ts`, so the map either is absent or points at sources that were never packaged. Inside a monorepo the sources are always there, which is why the option pays off most for internal projects.

saying these in an interview costs you the question

  • Confuses declarationMap with sourceMap
  • Sets declarationMap in the consuming project
  • Thinks it changes what the compiler type-checks
  • Expects navigation to work without building the referenced project
  • Believes editing the generated .d.ts is a valid fix

context