Using Declaration Maps for a Better Developer Experience
AI generated
<T>
type
TypeScript · Declaration Maps · Tooling · IDE
Using Declaration Maps for a Better Developer Experience
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.

16 min read declarationMap · d.ts.map · source maps · monorepo TypeScript 5.x · VS Code · WebStorm

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.

11. FAQ: Declaration Maps

1Declaration maps vs. source maps?
Source maps for runtime debugging, declaration maps for IDE navigation from .d.ts back to source.
2Why an error without declaration?
Without generated .d.ts files there is nothing for a map to point to, TypeScript refuses the combination.
3Do they work with npm packages?
Only if src/ is published too, otherwise the referenced source file does not exist in node_modules.
4Does src/ increase package size a lot?
Barely, TypeScript source files are usually smaller than compiled output. npm pack --dry-run shows it beforehand.
5Why does Go to Definition still land in .d.ts?
Partly intentional two step behavior in VS Code, partly broken relative paths in the map file.
6What does composite: true do?
Marks a project for project references, a prerequisite for cross-package navigation in monorepos.
7Can maps get lost during bundling?
Yes, if the bundling tool does not explicitly support declaration maps.
8Only relevant for published libraries?
No, internal monorepo packages benefit just as much from cross-package navigation.
9Do all IDEs support this equally?
Basically yes, the exact navigation behavior differs slightly between VS Code and JetBrains IDEs.
10How do I check if declaration maps are active?
Look in dist/ for .d.ts.map files next to the .d.ts files, if missing, the option was not active.