Open-Source Wikis

/

TypeScript

/

How to contribute

/

Debugging

microsoft/TypeScript

Debugging

Most TypeScript compiler bugs are reproduced via a failing test under tests/cases/. This page covers the debugging tools the team uses once a test is in hand.

printf debugging

The fastest path to understanding the checker is console.log. The repo's coding-guideline document recommends:

function checkSomething(n: Node) {
    doSomething(n);
+   // @ts-ignore DEBUG CODE ONLY, REMOVE ME WHEN DONE
+   console.log(`Got node with pos = ${n.pos}, kind = ${SyntaxKind[n.kind]}`);
    doSomethingElse(n);
}

Indexing back into the enum (SyntaxKind[n.kind]) gives a readable name. The same trick works for TypeFlags, SymbolFlags, NodeFlags, etc.

Always prefix debug logs with a comment that says they're temporary and remove them before opening a PR.

Debug.assert* and Debug.fail

The compiler ships rich debug primitives in src/compiler/debug.ts:

  • Debug.assertNever(x) — exhaustiveness check.
  • Debug.assertIsDefined(x) / Debug.checkDefined(x) — null check with assertion.
  • Debug.fail(message) — unconditional failure.
  • Debug.assert(condition, message?) — guarded assertion.
  • Debug.formatSyntaxKind(kind), Debug.formatTypeFlags(flags), Debug.formatSymbolFlags(flags) — pretty-print enums.

Use these instead of assert from "node:assert" or hand-rolled if (!x) throw …. The local ESLint rule debug-assert enforces this.

Inspector debugging

hereby runtests --tests=mytest -i (or --inspect) launches mocha under node --inspect-brk. Attach Chrome DevTools (chrome://inspect) or VS Code (use the bundled "JavaScript Debug Terminal").

For language-service debugging via fourslash, the same flag works — every fourslash assertion runs in the same Node process.

VS Code launch configurations

The repo includes /.vscode/launch.template.json. Copy to .vscode/launch.json, then choose:

  • Run currently open compiler test — runs the .ts test file in the active editor.
  • Run currently open fourslash test — runs an LS test.
  • Mocha Tests (currently opened test) — for unit tests.

Set breakpoints anywhere in src/. The launch config compiles incrementally so subsequent runs are fast.

Tracing

tsc --generateTrace ./trace-output produces Chrome-trace JSON files describing every checker phase, transformer pass, and emit step. Open chrome://tracing and load trace.json to see a flame graph.

tracing is wired into the source via the helpers in src/compiler/tracing.ts:

tracing?.push(tracing.Phase.Check, 'checkSourceFile', {
  fileName: file.fileName,
});
try {
  /* expensive work */
} finally {
  tracing?.pop();
}

Performance regressions are usually first noticed in trace output.

--diagnostics and --extendedDiagnostics

tsc --diagnostics ./tsconfig.json prints per-phase timings and counts:

Files:                       54
Lines of Library:         33812
Nodes of Library:        125698
Identifiers:              52371
Symbols:                  43126
Types:                    39084
Memory used:           187168K

--extendedDiagnostics adds per-file resolution counts and cache-hit rates.

Source maps

built/local/ is built with source maps. Stack traces map back to the original .ts files when running the local build. If they don't, check NODE_ENV=development and ensure setBlocking() hasn't been disabled.

Scoping a problem

Several patterns are useful when a bug is hard to localise:

  • Bisect via --target. If the bug only reproduces at --target ES5, look at transformers/es2015.ts, transformers/generators.ts, and helpers.
  • Bisect via --module. If only one module mode reproduces, suspect src/compiler/transformers/module/ and moduleNameResolver.ts.
  • Toggle --strict flags. Narrowing-related bugs often only appear under --strictNullChecks.
  • Try the LKG compiler. node ./lib/tsc.js runs the bootstrap compiler. If the LKG is fine and built/local is broken, the bug is in your in-progress changes.

Common pitfalls

  • Debug.assertDefined triggers in --isolatedModules. Some helpers expect a checker; in transpileModule paths the checker isn't available. Use Debug.checkDefined selectively.
  • SourceFile.path vs fileName. Map keys must use path (normalised). Many bugs come from accidentally keying on fileName.
  • Forgetting setParentRecursive. Synthesised trees without parent pointers will explode in the checker. The factory's update* methods preserve parent pointers; create* methods do not (they leave parent for the caller to set).

Performance

Common performance investigations:

  • A slow checker call usually corresponds to deep recursion in isTypeAssignableTo or instantiateType. Add a tracing.push to the suspected entry point and view the trace.
  • Slow auto-import is usually a node_modules scan. Profile under --generateTrace; look for the AutoImportProvider phases.
  • Slow watch-mode rebuilds correlate with cache misses in resolutionCache.ts. Add --traceResolution to see what's being looked up.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Debugging – TypeScript wiki | Factory