TypeScript Diagnostics: Reading the Numbers

The four tsc flags that tell you what TypeScript is doing — and what to do with the numbers they surface.

March 22, 20265 min read2 / 3

Before you can fix TypeScript performance, you have to know what's actually slow. The good news: TypeScript ships its own diagnostic tools. The bad news: most people don't know they exist until they're deep in a multi-hour debugging session.

There are four flags I reach for, in roughly this order.

--extendedDiagnostics

This is the first stop. It adds a timing and memory breakdown to your normal type check output:

Bash
tsc --extendedDiagnostics

You'll see output like:

Plain text
Files: 1,247 Lines of Library: 37,842 Lines of Definitions: 162,891 Lines of TypeScript: 89,441 Lines of JavaScript: 0 Lines of JSON: 2,103 Lines of Other: 0 Identifiers: 423,182 Symbols: 318,491 Types: 204,762 Instantiations: 1,847,293 Memory used: 412,284K Assignability cache size: 184,221 Identity cache size: 4,832 Subtype cache size: 2,104 I/O Read time: 0.18s Parse time: 1.24s ResolveModule time: 0.31s ResolveLibrary time: 0.09s Program emit time: 0.04s Total time: 3.18s

The numbers that matter most:

  • Files: If this is dramatically higher than you expect, something is pulling in files you didn't intend. This is where barrel file problems show up.
  • Instantiations: The number of times TypeScript had to instantiate generic types. High instantiation counts are the fingerprint of complex conditional types.
  • Memory used: If this is over 512MB, you're in trouble on standard machines. Over 1GB and you'll start seeing the TypeScript server crash.

Running this on CI before and after a change gives you a baseline to catch regressions early.

--listFiles

When the Files count in --extendedDiagnostics is higher than expected, this flag tells you exactly which files TypeScript is checking:

Bash
tsc --listFiles

The output is a flat list of every file included in the program. Pipe it somewhere useful:

Bash
tsc --listFiles 2>&1 | grep node_modules | wc -l

If that returns anything above zero with skipLibCheck: false, you're doing unnecessary work. More commonly, --listFiles reveals that your tsconfig.json root is pulling in test files, scripts directories, or entire packages you didn't mean to include.

The fix is almost always tightening the include array in your tsconfig.json:

JSON
{ "include": ["src/**/*.ts", "src/**/*.tsx"] }

Not ".". Not "**/*". Specific directories.

--explainFiles

--listFiles tells you what's included. --explainFiles tells you why:

Bash
tsc --explainFiles

The output looks like:

Plain text
../../node_modules/@types/lodash/index.d.ts Library referenced via 'lodash' from file 'src/utils/format.ts' Matched by include pattern '**/*' in 'tsconfig.json'

This is invaluable for the "I don't know why TypeScript is including this" problem. You'll often find that a single import somewhere in your codebase is pulling in a giant type definition file you didn't expect — and this tells you exactly which import is responsible.

It's also how you diagnose barrel file cascades. When you see a single component file that has a chain of 40 files below it in --explainFiles, that's a barrel file pulling in the entire module graph.

--generateTrace

The most powerful tool, and the most involved. This generates a performance trace file compatible with Chrome DevTools:

Bash
tsc --generateTrace ./trace-output

It creates two files in the specified directory: trace.json and types.json.

To read it:

  1. Open Chrome and navigate to chrome://tracing
  2. Load the trace.json file
  3. Look at the timeline

You'll see a waterfall breakdown of every phase TypeScript went through — which files took longest to parse, which types took longest to instantiate, which modules took longest to resolve.

The types.json file is a separate view: a catalogue of every type TypeScript constructed during the check, with its ID and how it was built. When you have a type that's being instantiated thousands of times, this is how you find it.

It's genuinely the most useful tool for diagnosing deep type complexity issues. The output format is the same one used for profiling JavaScript in Chrome DevTools — so if you've read flame charts before, this is immediately familiar.

The Diagnostic Workflow

Here's how I use these in practice:

  1. Establish a baseline: Run --extendedDiagnostics on main, save the numbers
  2. Identify the problem category:
    • High file count → --listFiles then --explainFiles
    • High instantiation count → --generateTrace
    • High memory → usually barrel files or complex generics; check both
  3. Make a targeted change (not three changes at once)
  4. Measure again with --extendedDiagnostics

The temptation is to guess at the problem and try a bunch of fixes simultaneously. That makes it impossible to know what actually helped. One change, measure, repeat.

The diagnostic tools work the same way whether the slowdown is in local type checking or CI — which is useful, because CI is often where the problem is worst and where it's hardest to debug interactively.

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.