Command Palette

Search for a command to run...

Hectal
PHASE 12Intermediate ~24 min· topic 6 of 6

Topic 12.6

Compiler Performance and Diagnostics

In one line

When the type checker feels slow you can measure it: --extendedDiagnostics counts the work, --generateTrace shows where the time went, and a few habits keep types cheap. TypeScript 7's native compiler is about ten times faster, and this topic also covers how to upgrade from 5.x through 6.0 to 7.0.

Think of it like this

A car feels slow. You don't guess: you look at the dashboard (speed, fuel, engine temperature), and if that isn't enough the mechanic plugs in a diagnostic computer that records what each part did. --extendedDiagnostics is the dashboard and --generateTrace is the diagnostic recording. Only then do you decide whether the fix is a new tyre (a smaller type) or a bigger engine (the faster compiler).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Diagnostics
Numbers and timings that tsc prints about its own work.
Instantiation
Filling in a generic type with real types, such as turning Array<T> into Array<string>. Each costs a little time.
Trace
A recording of what the checker did and when, written by --generateTrace.
TS2589
The error 'Type instantiation is excessively deep and possibly infinite': a recursive type that never reaches its end.
skipLibCheck
Do not type-check .d.ts files. Faster, and hides errors in other people's declarations.
Checker (7.0)
A worker thread that type-checks part of a project. --checkers sets how many.
ignoreDeprecations
A 6.0 setting that silences deprecation errors. It has no effect in 7.0, where the options are gone.
Bridge release
A version whose main job is to prepare for the next one. TypeScript 6.0 is the bridge to 7.0.

Step by step

01Measure first: --extendedDiagnostics

Run tsc --noEmit --extendedDiagnostics in a small project. These are real numbers from one run on one laptop: they will differ on yours, and the project is tiny, so the time is mostly startup and reading the standard library (64 files, almost all of them lib.*.d.ts).

Notice that TypeScript 7.0.2 prints fewer lines than 6.0.3 and that its memory number is much lower. Don't compare the two columns as a benchmark: the counters are measured differently.

terminalwhole filebash
$ tsc --noEmit --extendedDiagnostics          # TypeScript 7.0.2
Files:              64
Lines:           56167
Identifiers:     48117
Symbols:         55087
Types:           31890
Instantiations:  28137
Memory used:    61352K
Memory allocs:  252654
Config time:    0.004s
Parse time:     0.103s
Bind time:      0.063s
Check time:     0.670s
Total time:     0.900s

$ node node_modules/typescript-6/bin/tsc --noEmit --extendedDiagnostics   # 6.0.3
Files:                          64
Lines of Library:            56131
Lines of TypeScript:            23
Identifiers:                 48117
Symbols:                     53097
Types:                       27960
Instantiations:              27069
Memory used:               156025K
I/O Read time:               0.04s
Parse time:                  0.44s
Program time:                0.51s
Bind time:                   0.15s
Check time:                  1.87s
Total time:                  2.53s

02Find the cause: --generateTrace

--generateTrace trace writes a folder. Open trace.json in Perfetto or chrome://tracing to see the timeline; open the types_*.json files to see the types the checker built. In 7.0 there is one file per checker (four by default).

terminalwhole filebash
$ tsc --noEmit --generateTrace trace
$ ls trace
legend.json
trace.json
types_0.json
types_1.json
types_2.json
types_3.json

03skipLibCheck: someone else's broken declaration

broken.d.ts refers to a type that doesn't exist. It could be a package you installed. With skipLibCheck: false the error is yours; with true it is skipped, and the file that uses it still compiles.

terminalwhole filebash
# broken.d.ts:   export interface Widget { size: Missing; }
# tsconfig: "skipLibCheck": false
$ tsc
broken.d.ts(2,9): error TS2304: Cannot find name 'Missing'.

# "skipLibCheck": true
$ tsc
(no output, exit 0)

04Upgrade: the same old config on 6.0 and 7.0

An old config with target: es5, moduleResolution: node and esModuleInterop: false. 6.0 reports three deprecations (exit code 2) and tells you how to silence them. 7.0 reports three removals (exit code 1) with no way back. The right fix is to change the config, not to silence it.

terminalwhole filebash
$ node node_modules/typescript-6/bin/tsc
tsconfig.json(1,30): error TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
tsconfig.json(1,55): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
  Visit https://aka.ms/ts6 for migration information.
tsconfig.json(1,80): error TS5107: Option 'esModuleInterop=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

$ node node_modules/typescript-7/bin/tsc
tsconfig.json(1,30): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
tsconfig.json(1,55): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(1,80): error TS5108: Option 'esModuleInterop=false' has been removed. Please remove it from your configuration.
Upgrade: the same old config on 6.0 and 7.0diagram
Rendering diagram…

05The new types default shows up as missing names

In 6.0 and 7.0, types defaults to an empty list, so global names from @types/node are no longer picked up automatically. Code that uses process or require gets TS2591. The fix is exactly what the message says: install @types/node and list it.

terminalwhole filebash
$ tsc            # file uses process.argv and require("fs"), no "types" entry
t.ts(1,13): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
t.ts(2,19): error TS2591: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

# fix:  npm i -D @types/node   and in tsconfig:  "types": ["node"]

06A recursive type that never ends

The two live examples below build a list of zeros of a given length with a recursive type. At length 10 it is fine, and you can see the answer. At length 2000 the compiler stops with TS2589 and the type becomes any-like for that line. This is a performance guard, so you can't make the checker run forever.

Try it yourself

  1. 1

    Find the limit

    In the first example, change Count<2000> to smaller values (Count<500>, Count<999>, Count<1000>) and note when the error appears: in TypeScript 7.0.2 Count<999> is clean and Count<1000> fails. The exact limit is an internal detail that can change between versions, so never design around it.

  2. 2

    Read the counters

    In any project of yours run tsc --noEmit --extendedDiagnostics. Write down Files, Types, Instantiations and Check time. Then add skipLibCheck: true and run again: which numbers change?

  3. 3

    Read the upgrade errors

    Take the old config in the walkthrough, and for each of the three options write the replacement you would choose (target: your oldest engine, es2015 or later; moduleResolution: bundler or nodenext; esModuleInterop: leave it out, it is always on). Then run both compilers on it.

Code & diagrams

Recursion that is fine at 10 and too deep at 2000

Count builds a tuple of N zeros one step at a time. Ten steps are fine. At 2000 steps the compiler hits its depth limit and reports TS2589 on the line that asks for it. The code still runs because types are erased: the error is a warning about the type, not about the program.

Console

(nothing was printed)

Expected console output

0
A type that builds a string 1000 letters long

Another type that counts to 1000 while it grows a string. The checker refuses at that depth. The practical lesson: type-level loops are for small inputs. When a type needs thousands of steps, compute it at runtime instead, or change the design so the type stays flat.

Console

(nothing was printed)

Expected console output

x

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Recurse a few thousand times

Ask for Count<2000> in the first live example.

terminal
$ Types panel
── what you'll see ──
index.ts(5,16): error TS2589: Type instantiation is excessively deep and possibly infinite.

Break #2

Take a 5.x config straight to 7.0

Run TypeScript 7.0.2 on a config with "target": "es5".

terminal
$ tsc
── what you'll see ──
tsconfig.json(1,30): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.

Myth vs fact

Myth

TypeScript 7.0 changes how types are checked.

Fact

Type checking is structurally identical to 6.0. What changed is the implementation (native code, parallel) and a set of removed options and new defaults.

Myth

skipLibCheck makes my own types unchecked.

Fact

It skips only .d.ts files. Your .ts files are fully checked, but declaration files you wrote by hand are skipped too.

Myth

TS2589 means the compiler crashed.

Fact

It is a deliberate depth limit with a normal error message. Your tool keeps working.

Myth

A slow build is always the compiler's fault.

Fact

Often it is one expensive type, too many files in include, or @types packages you never use. Measure with --extendedDiagnostics before blaming the compiler.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The checker caches type relations (is A assignable to B?). Giant unions and many structurally similar large object types defeat the cache, because each pair has to be compared. Naming types, using interface ... extends and keeping unions small keep relation work cheap.

  • ▸

    In 6.0, --stableTypeOrdering makes the order of union members match what 7.0 prints. It is useful during the upgrade: if error messages differ between the two compilers only in the order of union members, turn this flag on in 6.0 and compare again. In 7.0 the behaviour is always on.

  • ▸

    Performance numbers in release announcements are measured on large real repositories. A project with a few hundred files will not feel a ten times difference in total time, because startup and reading the standard library are a fixed cost. What you will notice is the editor: first error and go-to-definition become near-instant on big projects.

  • ▸

    Since 7.0 has no programmatic API yet (expected in 7.1), anything that imports typescript as a library (lint rules with type information, framework language plugins, code generators) must keep depending on 6.0. Pin both versions explicitly in package.json and check which one each tool picks up.

Remember this

  1. 1

    The problem this solves: as a project grows, checking can become slow, and guessing at the cause wastes time. This topic gives you tools to measure where the time goes and a safe path for upgrading the compiler itself. Type checking has a cost that grows with the number of files read, the number of types created and the number of instantiations (each time a generic type like Array<T> is filled in with a real type). Most projects are dominated by reading library .d.ts files and by a few expensive types. **--extendedDiagnostics** prints counters for all of that plus the time spent in each stage: parsing, binding (connecting names to declarations), checking and emitting. Always measure before you change anything.

  2. 2

    In practice the numbers to watch are Files (are you accidentally including node_modules sources or whole @types folders?), Types and Instantiations (a sudden jump after a change points to one expensive type), and Check time (almost always the biggest). TypeScript 7.0 prints a shorter list than 6.0, because it has a different internal design, so compare numbers within one version, never across them.

  3. 3

    **--generateTrace <dir>** writes a performance trace: trace.json (a timeline you can open in Chrome's chrome://tracing or in Perfetto) and types_*.json (a list of types the checker created). In TypeScript 7.0 you get one types_N.json per checker. The timeline shows which file and which check took long, and the types list shows the huge unions or deeply nested generics behind it. TypeScript's own performance guide describes how to read it.

  4. 4

    Things that make types slow, from most to least common: giant unions (thousands of members, where each comparison walks all of them); deeply recursive conditional or mapped types; intersections of large object types repeated everywhere (interface ... extends is cheaper because the result is cached by name); and inferred return types of huge exported functions that the checker has to compute again in every file that uses them. The cure is usually to name an expensive type with a type alias or interface and to annotate exported return types, so work is done once.

  5. 5

    There is also a built-in safety limit. When instantiation goes too deep the compiler gives up with **TS2589: Type instantiation is excessively deep and possibly infinite**. It is not a crash: it is the checker telling you a recursive type does not stop soon enough. The live examples below hit it with a counter built from a recursive type. Fixes: add a base case, recurse less deeply, split the work, or rewrite so the recursive call is the last thing in the branch (a tail-recursive conditional type), which lets the compiler loop instead of nesting and allows far more steps than plain nesting does. The counter below is already written that way, so it hits the 1000-step limit: Count<999> is fine and Count<1000> fails.

  6. 6

    **skipLibCheck** tells tsc not to type-check any .d.ts file. A broken declaration inside a package you installed then stops being your error, and the check is faster. It is in the tsc --init file because the types in a library file are the library authors' concern, not yours. The price: a mistake in your own hand-written .d.ts (Topic 11.3) is hidden too, and you give up detecting two libraries whose types disagree with each other. Keep it on in apps; for a library you publish, check your own declarations once in CI with it off.

  7. 7

    TypeScript 7.0 changes the numbers more than any tuning: the compiler was ported to Go, so it runs as native code on several threads. The announcement reports about a ten times speed-up (VS Code's repository: 125.7 s down to 10.6 s). Type-checking results are the same as 6.0. Parallelism is controlled by --checkers (checker workers per project), --builders (projects built in parallel, Topic 12.4) and --singleThreaded (turn it all off, handy for comparing or for tiny CI machines). The numbers on your project will differ from the headline: it depends on project size, core count and disk.

  8. 8

    Upgrading is a two-step path. From 5.x go to 6.0 first: it still runs on JavaScript, accepts old options with warnings, and has new defaults (strict: true, types: [], module: esnext, rootDir = the tsconfig folder). Fix every deprecation it reports. Then move to 7.0, where the deprecated options are removed (TS5108, TS5102). "ignoreDeprecations": "6.0" silences the warnings in 6.0 only; it is a deadline, not a solution. Tools that use the compiler's programmatic API (typescript-eslint, framework language tools) can't use 7.0 until it gets an API, so they use 6.0 through @typescript/typescript6 (its executable is tsc6), side by side with 7.0 for tsc.

Explain it without notes

01

How would you investigate a type checker that has become slow?

02

What is TS2589 and how do you fix it?

03

What does skipLibCheck do and what do you lose?

04

What changed in TypeScript 7.0 and what stayed the same?

05

Describe the upgrade path from TypeScript 5.x to 7.0.

Practice

01

A teammate says tsc went from 4 seconds to 12 seconds after a pull request. List the commands you run, in order, and what each tells you.

02

Fix this config for TypeScript 7.0. Write the new version and one sentence per change. ``json { "compilerOptions": { "target": "es5", "moduleResolution": "node", "baseUrl": "./src", "outDir": "dist" } }

Trade-offs

  • ↔

    Aggressive type-level programming gives precise APIs but costs checking time and can hit TS2589. A simpler, slightly less precise type is often the better engineering trade.

  • ↔

    Staying on 6.0 keeps every tool that uses the compiler API working; moving to 7.0 gives a large speed-up now. Many teams run 7.0 for tsc and 6.0 for the tools, until the 7.x API lands.

Done when you can

  • I can use --extendedDiagnostics and say which numbers matter.

  • I can produce a trace with --generateTrace and know what the files in it are.

  • I can explain TS2589 and name three ways to avoid it.

  • I can explain skipLibCheck and what it hides.

  • I can describe the 5.x to 6.0 to 7.0 upgrade and the role of ignoreDeprecations.

  • I know that 7.0 checks types like 6.0 but runs natively, and that API-based tools stay on 6.0.