Debugging TypeScript

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

Debugging TypeScript has two sides: debugging the JavaScript that actually runs, and debugging the type facts the editor and compiler believe. Source maps connect the first side back to your .ts files; editor navigation and compiler explanation flags help with the second.

This lesson is mostly tool-shaped. The debugger and launch examples are read-only project files. The one playground is only for the type-hover habit, not for pretending to launch VS Code.

Source maps: stepping through TS while JS runs

TypeScript erases to JavaScript before runtime. A debugger pauses in a running JavaScript process, but you want breakpoints and stack frames to point at the TypeScript source you wrote. A source map is the small mapping file that lets the debugger translate between those two views.

For a tsc-emitted Node project, the basic setting is:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "sourceMap": true,
    "strict": true
  },
  "include": ["src/**/*.ts"]
}

If your project uses a bundler or framework, the source-map option may live in that tool instead. The principle stays the same: the debugger can only step through original TypeScript when the build output includes usable maps and VS Code can find the generated JavaScript.

For compiled output, outFiles is the usual clue VS Code needs:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug built Node app",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/src/server.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

program names the source you care about. preLaunchTask builds it. outFiles tells the debugger where to look for the JavaScript and source maps that actually run.

Launch configs you will actually keep

A launch.json is just saved debugger setup. You can have several configurations because scripts, tests, and browser apps start differently.

Reference launch.json shapes:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug current TS file with tsx",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "node",
      "runtimeArgs": ["--import", "tsx"],
      "program": "${file}",
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "name": "Debug current Vitest file",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
      "args": [
        "run",
        "${relativeFile}",
        "--test-timeout=0",
        "--no-file-parallelism"
      ],
      "console": "integratedTerminal",
      "autoAttachChildProcesses": true,
      "smartStep": true,
      "skipFiles": ["<node_internals>/**", "**/node_modules/**"]
    },
    {
      "name": "Launch browser app",
      "type": "msedge",
      "request": "launch",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

Treat these as starting points, not sacred snippets. The tsx config runs the current file with Node and the tsx loader. The Vitest config follows the same shape as Vitest's debugging docs: run one test file, avoid parallel test files, and remove the timeout while you are stopped at breakpoints. The browser config assumes your dev server is already running at that URL.

Use names that describe the workflow, not the tool: "Debug current Vitest file" is better than "Node Launch 2". Future you will thank present you when the debug dropdown has five choices.

Debugging the types

Runtime debugging answers "what value is here?" Type debugging answers "what does the checker believe is here?"

Your first tools are boring and powerful:

  • Hover a symbol to see its type and documentation.
  • Go to Definition to find the implementation or declaration.
  • Go to Type Definition when a value is shaped by an interface, alias, or library declaration.
  • Use Rename Symbol only after the first three prove you are looking at the right symbol.

When a hover is too compressed, rebuild the derived shape with the Prettify<T> helper from Section 12:

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

→ Mina - pro

The runtime output is intentionally dull. The point is the hover: compare PublicProfileRaw with PublicProfile. The first shows the recipe; the second is usually easier to scan as the resulting object shape. Prettify does not change requirements. It changes how the editor presents a type you already built.

This is also where "Go to Type Definition" pays off. If profile.plan is not the union you expected, jump to the type behind profile, then keep following aliases until you find where the union widened or narrowed.

When builds mystify you

Sometimes the confusing part is not one line of code. It is the project graph: why is this file included, why is this declaration visible, why did this path resolve to that package?

These commands are the useful flashlight set:

npx tsc --showConfig
npx tsc --explainFiles
npx tsc --listFilesOnly
npx tsc --traceResolution

--showConfig prints the final config after extends and defaults are applied. Use it when the tsconfig you are reading is not the config TypeScript is really using.

--explainFiles prints the files in the compilation and why each one got there: matched by include, pulled in by a referenced library, imported by another file, and so on.

--listFilesOnly is the shorter "what files are in this program?" version. Use it when you do not need explanations yet.

--traceResolution is loud on purpose. It prints the module-resolution search process, which is exactly what you need when paths, package exports, moduleResolution, or declaration packages are sending TypeScript somewhere surprising.

The debugging order is practical: first reproduce the problem, then inspect the value, then inspect the type, then inspect the project graph. Jumping straight to config flags before you have a small failing case is how debugging becomes archaeology.

Next lesson stays with tooling and looks at compiler performance: why type checking slows down, which flags expose the cost, and how to keep large projects responsive.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion