skip to content

Your CI step that runs tsc --noEmit has grown to several minutes on a mid-sized TypeScript repo. Which compiler flags and measurements would you use to find where the time is actually going?

level: seniorimportance: should knowfreq 36%

answer

  1. measure before you change anything
  2. the compiler ships its own instrumentation
  3. file count versus instantiation count
  4. a trace file plus an analyzer package

basics

~20 s

Start with tsc --extendedDiagnostics for a breakdown of parse, bind, check and emit time plus file, type and instantiation counts. Then use --generateTrace with @typescript/analyze-trace to find the specific files and types that dominate checking.

solid answer

~50 s

Measure before changing anything. `tsc --noEmit --extendedDiagnostics` prints the shape of the problem: how many files are in the program, counts of types and instantiations, and time split across parse, bind, check and emit. Two very different diagnoses come out of that — a huge file count means the program includes more than you think, and a huge instantiation count with heavy check time means expensive type-level code. For the first, `--listFiles` and `--explainFiles` tell you which files are in and *why* they were pulled in, which usually surfaces a stray `include` glob, an unexpected `types` entry, or an unwanted `node_modules` directory. For the second, `--generateTrace <dir>` writes a trace that Microsoft's `@typescript/analyze-trace` package turns into a ranked list of the slowest files and type instantiations. `skipLibCheck` is worth checking too, since declaration-file checking can be a large share.

code

bash · 9 lines
bash
# Triage: counts and a parse/bind/check/emit time split.
npx tsc --noEmit --extendedDiagnostics

# Branch A - is the program bigger than you think?
npx tsc --noEmit --explainFiles > files.txt

# Branch B - which types are expensive?
npx tsc --noEmit --generateTrace traceDir
npx @typescript/analyze-trace traceDir

go deeper

for a junior

Know that the TypeScript compiler can report its own timings, and that a slow check is investigated by measuring rather than by turning the check off.

for a middle

Name --extendedDiagnostics and describe what it reports: file, type and instantiation counts plus a parse/bind/check/emit time split. Explain what skipLibCheck stops the compiler doing.

for a senior

Demonstrate the branching diagnosis — too many files versus too-expensive types — and name the tool for each: --explainFiles for program contents, --generateTrace with analyze-trace for hot types. Say what you would change after each finding.

for a principal

Set the policy: the gate stays blocking, latency is solved by parallelism and by fixing real hot spots, and diagnostics numbers are tracked over time so a type-level regression is caught the week it lands rather than a year later.

## Why this problem exists at all In a split pipeline the bundler is fast and the checker is the slow half, so the type-check job becomes the long pole in CI. The temptation is to make it faster by making it weaker — dropping it to non-blocking, or narrowing what it checks. The professional move is to find out where the time goes first, because in practice a large fraction of slow check jobs are slow for a fixable and slightly embarrassing reason. ## Step one: --extendedDiagnostics ```bash tsc --noEmit --extendedDiagnostics ``` This prints a summary that includes the number of files, lines, nodes, identifiers and symbols, the number of **types** and **instantiations** created, memory used, and a time breakdown across parse, bind, check and emit (plus a total). Read it as a triage tool: - **Files far higher than expected** → the program is too big. You are checking things you did not mean to check. - **Instantiations in the millions with check time dominating** → the program is the right size but the type-level work is expensive: deeply recursive conditional or mapped types, enormous unions, heavily overloaded generic helpers. - **Large parse time relative to check** → sheer volume of source, often the same "too many files" story. ## Step two, branch A: what is even in this program? `--listFiles` prints every file included. `--explainFiles` goes further and prints, for each file, the reason it was included — an entry point, an import from a specific file, a `types` package reference, a root of the `include` glob. That "why" column is what turns a guess into a diagnosis. Classic findings: an `include` glob that reaches build output or fixtures, an unnecessary `@types` package pulled in globally, or a directory of generated code nobody meant to check. `--traceResolution` is the more granular cousin, printing how each module specifier was resolved — useful when the surprise is *which* copy of a package got loaded. Also check `skipLibCheck`. Without it, the compiler checks the declaration files of your dependencies against each other; with a large dependency tree that can be a serious share of the total. It is a common and generally accepted setting for application code precisely because of this cost. ## Step two, branch B: what type is so expensive? ```bash tsc --noEmit --generateTrace traceDir npx @typescript/analyze-trace traceDir ``` `--generateTrace` writes trace output into the given directory; `@typescript/analyze-trace` (published by the TypeScript team) reads it and reports hot spots — the files that took longest to check and the specific type instantiations responsible. This is how you find the one utility type that a hundred call sites instantiate recursively, or the single enormous union that everything is compared against. The fixes that follow are type-level, not build-level: give an expensive inferred type an explicit annotation so the checker stops recomputing it, cap a recursive type's depth, replace a giant conditional chain with a lookup, or split a monstrous union. ## What good practice looks like Measure, form a hypothesis, change one thing, measure again with the same flag. Keep the numbers — a `--extendedDiagnostics` line in CI logs is a cheap regression detector, because the instantiation count usually jumps the day someone lands a clever type. And hold the line on the gate itself. Making the check non-blocking, or excluding the tests from it, converts a latency problem into a correctness problem: the job gets faster and stops telling you anything. If latency is genuinely unacceptable, run the check in parallel with the build job rather than shrinking what it covers. ## The judgment being tested An interviewer asking this wants to see that you reach for measurement rather than folklore, that you know the compiler ships its own instrumentation, and that you can distinguish "the program is too big" from "the types are too expensive" — because those two diagnoses have completely different fixes and confusing them wastes days.

  • --extendedDiagnostics shows 40,000 files but your src directory holds 900. What do you look at next?
    `--explainFiles`, because it prints why each file entered the program. The usual culprits are an `include` glob reaching generated output or fixtures, an `@types` package pulled in wholesale, or a dependency shipping unbundled TypeScript sources. Fix the inclusion rule rather than trying to make checking of unwanted files faster.
  • When is skipLibCheck the wrong thing to reach for?
    When you are the one publishing the declaration files. Skipping declaration checking hides genuine conflicts between the `.d.ts` files you ship and their dependencies, which your consumers will then hit instead of you. For an application it is a reasonable cost saving; for a library it can export the problem.
  • Your trace analysis blames one generic helper used everywhere. What kinds of fix apply?
    Reduce the work per instantiation rather than the number of call sites: annotate the return type explicitly so the checker stops re-deriving it, bound a recursive conditional's depth, replace a long conditional chain with an indexed lookup, or split an oversized union. Each cuts instantiations without weakening the gate.

saying these in an interview costs you the question

  • Making the type-check job non-blocking to make it fast
  • Assuming the slowdown must be the machine or CI runner
  • Guessing at a cause without running any diagnostics
  • Excluding tests from the check purely for speed
  • Thinking skipLibCheck skips checking of your own code

context