jumping straight from .d.ts back to the original code
Anyone who ctrl clicks an imported type and lands in a generated .d.ts file instead of their own source code loses valuable debugging time. Declaration maps close exactly that gap and turn a TypeScript library into one that feels, inside the IDE, as if its source code lived right inside your own project.
Table of Contents
- 1. The problem: ctrl click lands in the generated .d.ts
- 2. What declaration maps technically are
- 3. Enabling declarationMap in tsconfig.json
- 4. How declarationMap and sourceMap work together
- 5. How IDEs actually use declaration maps
- 6. Declaration maps when publishing a library
- 7. Pitfalls: relative paths and moved build folders
- 8. Monorepo scenario: declaration maps across package boundaries
- 9. With and without declaration maps compared
- 10. Summary
- 11. FAQ
1. The problem: ctrl click lands in the generated .d.ts
Anyone who uses a TypeScript library in a foreign project and jumps to the definition of an imported type via ctrl click or cmd click lands, by default, in the generated .d.ts file inside the library's dist/ folder. That file contains pure type signatures without implementation, without contextual comments and without the original structure of the source code. For a quick lookup that is fine, for deeper research into your own library or debugging a type level problem it falls short.
It becomes especially annoying when developers work on their own TypeScript library and switch between several packages of a monorepo. Without declaration maps, the IDE jumps into the compiled .d.ts on every import of an internal package, even though the source code is just one folder away. That costs time on every single navigation step and interrupts the flow of thought, especially in large codebases with many internal dependencies.
2. What declaration maps technically are
Declaration maps work on the same principle as classic JavaScript source maps, just one level up: instead of mapping compiled JavaScript back to the original TypeScript source, they map generated .d.ts files back to the .ts source they were generated from. For every index.d.ts, the compiler produces an accompanying index.d.ts.map file containing position information in JSON format, referenced through a comment at the end of the .d.ts file.
This mapping lets IDEs, on "Go to Definition", not stop at the type signature but automatically take a further jump to the actual implementation in the source code. The result: developers navigate through a TypeScript library exactly the way they would navigate through their own application code, completely transparently, without ever noticing the difference between a compiled package and source code.
3. Enabling declarationMap in tsconfig.json
Enabling declaration maps requires only a single additional option in tsconfig.json, but has a prerequisite that is frequently overlooked: declaration: true must also be set, because without generated .d.ts files there is nothing for a declaration map to point to. In addition, sourceMap: true must be active so the chain from declaration map all the way to the actual .ts source file is fully closed.
An important practical note: declarationMap without declaration produces a compiler error, TypeScript then refuses the build entirely. Anyone wanting to enable this option in an existing project should therefore first check whether declaration is already set before adding the new option.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}
4. How declarationMap and sourceMap work together
Declaration maps and classic source maps solve two different problems that are frequently confused. A classic source map maps compiled JavaScript back to TypeScript source code at runtime, so stack traces and debugger breakpoints work against the original code. A declaration map, on the other hand, maps the static type information in the .d.ts file back to the source code at development time, so the IDE can navigate. Both mechanisms work independently but complement each other when a TypeScript library needs to be both runtime debuggable and navigable during development.
At the end of the generated index.d.ts file, a comment such as //# sourceMappingURL=index.d.ts.map appears, pointing to the accompanying map file. That map file in turn contains a sources entry that points, relative to the map file, to the original .ts file. If one of these relative paths gets shifted by a faulty build process, the chain breaks and the IDE silently falls back to plain .d.ts navigation without showing any error message.
// dist/index.d.ts.map — generated declaration map (simplified)
{
"version": 3,
"file": "index.d.ts",
"sourceRoot": "",
"sources": ["../src/index.ts"],
"names": [],
"mappings": "AAAA,cAAc,EAAE,MAAM,WAAW,CAAC"
}
5. How IDEs actually use declaration maps
Visual Studio Code supports declaration maps without extra configuration, provided the setting typescript.preferGoToSourceDefinition is enabled or default navigation is used. On the first jump, the cursor lands in the .d.ts file, a second call of "Go to Definition" or the alternative action "Go to Source Definition" then follows the declaration map all the way to the actual source. This two step behavior is intentional, because some developers deliberately want to see the public type signature before jumping deeper into the implementation.
JetBrains IDEs such as WebStorm and PhpStorm with the TypeScript plugin resolve declaration maps automatically on the first navigation attempt, without forcing an intermediate step through the .d.ts file. In both IDE families, the prerequisite is that the map files are actually present in the published package and that the referenced source files are reachable for the IDE, either through node_modules shipped with sources or through a locally linked monorepo package.
// .vscode/settings.json — prefer jumping straight to source, skip the .d.ts hop
{
"typescript.preferGoToSourceDefinition": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
6. Declaration maps when publishing a library
A frequently overlooked prerequisite: declaration maps only work for externally installed packages if the original .ts source files are actually included in the npm package. If the files field in package.json only publishes dist/, the declaration map correctly points to ../src/index.ts, but that file simply does not exist in the installed node_modules directory. The IDE then cannot navigate and falls back to the .d.ts view.
For a TypeScript library that should benefit from the full declaration map experience, the src/ folder therefore also needs to be included in the published package. The additional disk footprint is small in most cases, because TypeScript source files are typically smaller than compiled outputs with additional comments and formatting. Anyone who still wants to keep package size minimal can check with npm pack --dry-run which files actually end up in the tarball.
# Verify that src/ is actually part of the published tarball
npm pack --dry-run
# Example output should list both dist/ and src/
# npm notice ???? @mironsoft/query-builder@1.2.0
# npm notice === Tarball Contents ===
# npm notice 1.1kB dist/index.d.ts
# npm notice 0.3kB dist/index.d.ts.map
# npm notice 2.4kB dist/index.js
# npm notice 1.8kB src/index.ts
7. Pitfalls: relative paths and moved build folders
The most common mistake with declaration maps happens when rootDir and outDir in tsconfig.json are not consistent with the actual folder structure. If, for example, a build script is used that moves the output into a different folder afterward without adjusting the map files accordingly, the relative paths in .d.ts.map point nowhere. The IDE then shows no error, it simply does not navigate any further, which makes the problem hard to diagnose.
A second pitfall involves build tools that later merge declarations with a separate bundler for .d.ts files, for instance to merge several small .d.ts files into a single rolled up file. These tools need to explicitly support declaration maps, otherwise the mappings get lost during the merge. Before using such a tool, it is worth checking its documentation for whether declarationMap is supported at all.
8. Monorepo scenario: declaration maps across package boundaries
In a monorepo with several internal TypeScript packages, declaration maps show their biggest benefit. Through TypeScript project references with composite: true and declaration maps enabled, the IDE navigates on import of an internal package straight into that package's source code instead of its compiled dist/ folder. The prerequisite is that every project in the monorepo has composite: true set in addition to declarationMap: true, because project references require composite projects as a baseline requirement.
For teams that switch between multiple packages of a monorepo every day, this effect is noticeable: a bug fix in a shared utility package can be found and edited directly from the calling package, without manually switching folders or starting a file search. Declaration maps make a monorepo usable in practice the way it is supposed to feel: as one single, coherent project instead of a collection of isolated packages.
// packages/utils/tsconfig.json — required for cross-package navigation
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
}
}
9. With and without declaration maps compared
The difference between a TypeScript library with and without declaration maps does not show up in runtime behavior, only in day to day developer experience. The table below summarizes the key differences.
| Aspect | Without declaration maps | With declaration maps |
|---|---|---|
| Go to Definition | Lands in generated .d.ts | Jumps all the way to original source |
| Comments in source | Not visible | Fully visible at the origin |
| Monorepo navigation | Via dist/ per package | Straight between src/ folders |
| Package size | Minimal | Slightly larger due to src/ and .map |
The small extra package size is nothing compared to the time saved during everyday navigation, especially for teams actively developing a TypeScript library or working in a monorepo with many internal dependencies.
Another observation from practice: once a team gets used to declaration maps, their absence in another project immediately stands out negatively. That perceived step backward in navigation often motivates exactly the teams that were previously skeptical about the extra configuration effort to enable the option retroactively across more internal packages.
Mironsoft
TypeScript tooling, monorepos and developer experience
Need a TypeScript monorepo with better navigation?
We set up declaration maps, project references and build pipelines so your team navigates between internal packages as if everything were one single project.
tsconfig audit
Checking existing configuration for missing declaration maps and project references
Monorepo setup
Setting up composite projects and cross-package navigation
Publishing check
Verifying package tarballs so declaration maps also work for consumers
10. Summary
Declaration maps solve a concrete, everyday problem: navigating from a generated .d.ts file back to the actual source code of a TypeScript library. With declarationMap: true, declaration: true and sourceMap: true in tsconfig.json, enabling them takes just a few lines. When publishing, the src/ folder must also be shipped, otherwise the mapping leads nowhere for external consumers.
The biggest practical effect shows up in monorepos with many internal packages, where declaration maps together with project references enable seamless navigation across package boundaries. The small extra effort in configuration and package size is justified by the noticeably better developer experience, especially in teams that work with their own TypeScript library every day.
Declaration maps — the essentials at a glance
Enabling
Set declaration, declarationMap and sourceMap together in tsconfig.json.
Publishing
Publish the src/ folder too, otherwise map paths lead nowhere for consumers.
IDE behavior
VS Code and JetBrains IDEs follow the chain from .d.ts.map to the .ts source automatically.
Monorepo
Combined with composite: true and project references, the best cross-package navigation experience.