Skip to Content
TypeScript Advanced: Types That Hold Up in Production
Advanced
0 likes
Courses
TypeScript Advanced: Types That Hold Up in Production

Keep Build Times Sane: incremental and What Actually Slows tsc

Measure a build with extendedDiagnostics, then try skipLibCheck, a type that costs too much and incremental, with the numbers from a real run. After this, you change a build setting because you measured, not because you guessed.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already read --extendedDiagnostics output and only want what to change, skip to "Fix 1: skipLibCheck", then continue.

TypeScript 7 is much faster than the versions before it. The announcement quotes full builds that are 8 to 12 times quicker, and a small project now checks in a fraction of a second. That makes it tempting to stop caring about build time. I would not, for one reason: the speedup is a multiplier, and a project that was slow for a bad reason is still slow for the same reason, just 10 times less so. This lesson is about finding the reason, with numbers, before you change any setting.

The numbers in this lesson come from one run on the machine that built the course. Counts such as files and types are the same on any machine. Times are not, so read them as a comparison between two runs.

The test project

It has one small file that uses Express and Node types. A second file, with a large type, comes later.

{
  "name": "builddemo",
  "private": true,
  "type": "module",
  "devDependencies": {
    "typescript": "7.0.2",
    "@types/node": "^26.6.5",
    "@types/express": "^5.0.6"
  }
}
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "strict": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src"]
}
import express from "express";
import { readFileSync } from "node:fs";

export const app = express();
export const read = readFileSync;
npm install --no-audit --no-fund --loglevel=error

Measure first

--extendedDiagnostics prints what the compiler did. Four lines matter.

npx tsc --extendedDiagnostics | grep -E "^(Files|Types|Instantiations):"
Files:              202
Types:            79306
Instantiations:  101294

Files is how many files are in the program, including the lib.*.d.ts files that come with the compiler. Types is how many distinct types the checker created, and it is the best single measure of how hard it worked. Instantiations counts how often a generic type was filled in. Time is the line everyone looks at, but it moves with the machine. These three do not.

npx tsc --extendedDiagnostics | grep -E "^(Check time|Total time):"
Check time:      0.329s
Total time:      0.388s

Fix 1: skipLibCheck

Where do 200 files come from, when the project has one source file? Ask the compiler to list them and count by folder: the compiler's own lib files, @types, undici-types (which @types/node pulls in) and your src.

npx tsc --listFiles | grep -c "/lib/lib\."
npx tsc --listFiles | grep -c "/node_modules/@types/"
npx tsc --listFiles | grep -c "/node_modules/undici-types/"
npx tsc --listFiles | grep -c "/src/"
65
92
44
1

Almost everything is declaration files that you did not write. By default, the compiler checks them like your own code, on every run, even though you cannot change them. skipLibCheck turns that off. It stops the compiler from checking .d.ts files. Your own code, and how it uses the libraries, are still checked.

npx tsc --extendedDiagnostics --skipLibCheck | grep -E "^(Files|Types|Instantiations):"
Files:             202
Types:             348
Instantiations:      0

The files are the same. The types fall from tens of thousands to a few hundred, because the compiler no longer checks Express's and Node's own declarations. For a small project that saves a fraction of a second. For a large one it is often the single biggest saving, and it is the one setting I would turn on first.

⚠ Warning: skipLibCheck also hides a real mistake: two libraries whose declarations disagree. If a type from a package looks wrong, turn it off for one run and read what it reports.

Fix 2: find the type that costs the most

The other cause of a slow check is a type you wrote. Add src/codes.ts. It builds a four-digit code from ten digits, which is 10,000 members, and then makes a mapped type with 10,000 properties.

type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
export type Code = `${Digit}${Digit}${Digit}${Digit}`;
export type Table = { [K in Code]: { code: K; label: `code-${K}` } };

export const label: Table["1234"]["label"] = "code-1234";
npx tsc --extendedDiagnostics --skipLibCheck | grep -E "^(Types|Instantiations):"
Types:           11491
Instantiations:      3

Compare that with the previous count: 348 types became 11,491. A file of four short lines added about 11,000 types, which is more than the compiler had created for everything else once the libraries were skipped. Nothing looked wrong in the editor, because each line is small. When the numbers jump, --generateTrace shows where.

npx tsc --generateTrace trace --skipLibCheck
ls trace
legend.json
trace.json
types_0.json
types_1.json
types_2.json
types_3.json

trace.json opens in chrome://tracing or in Perfetto and shows how long each file took to check. types_0.json and its siblings list the types, so you can search for the one that has thousands of members. Then replace it with something coarser. A code like 1234 rarely needs ten thousand known members, and the number template type in the next file says the useful part in one line.

export type Code = `${number}`;
export type Table = Record<Code, { code: Code; label: string }>;

export const label: Table["1234"]["label"] = "code-1234";
npx tsc --extendedDiagnostics --skipLibCheck | grep -E "^(Types|Instantiations):"
Types:             361
Instantiations:      4

💡 Tip: Keep a record of the Types number for your repository, as you would for bundle size. A pull request that doubles it deserves a question, whatever the build time says.

Fix 3: incremental

incremental saves what the compiler learned into a .tsbuildinfo file. Next time it checks only the files that changed and the files that depend on them. It is on automatically with --build and composite. For a plain tsc run you turn it on.

rm -f tsconfig.tsbuildinfo
npx tsc --incremental --extendedDiagnostics | grep -E "^(Types|Total time):"
ls *.tsbuildinfo
Types:                  79315
Total time:            0.445s
tsconfig.tsbuildinfo

That first run did the full check, libraries included, and wrote tsconfig.tsbuildinfo. Run it again, then change one file and run it a third time.

npx tsc --incremental --extendedDiagnostics | grep -E "^(Types|Total time):"
echo "export const touched = 1;" >> src/app.ts
npx tsc --incremental --extendedDiagnostics | grep -E "^(Types|Total time):"
Types:                   340
Total time:           0.076s
Types:                   350
Total time:           0.086s

The second run found nothing changed and skipped almost everything. The third rechecked the one file I touched and what depends on it. Both are about a fifth of the first run, and the Types line shows why: the saved state already knows the library declarations are fine. Keep the .tsbuildinfo file out of version control, and in CI cache it between runs or the benefit is gone. On a small project like this the saving is a few tenths of a second. In --watch mode and in large repositories, where the first full check is the part that hurts, it is the difference between waiting and not waiting.

The other levers

  • Narrow the include list. A pattern that matches every file under the project reaches a scripts or fixtures folder checks files that never ship. Compare --listFiles with what you expected.
  • Project references. Splitting a repository into projects, as in the earlier lesson, lets tsc -b skip projects that did not change.
  • The checkers flags. TypeScript 7 checks with several workers. The default is four. More can be faster on a big machine and costs memory; fewer helps on a small CI runner. The --singleThreaded flag turns the parallelism off, which is useful for debugging and for comparing against TypeScript 6. Change these only after you have measured.

Common mistakes

  • Turning on incremental and committing the .tsbuildinfo file.
  • Setting skipLibCheck and forgetting why. Write the reason next to it.
  • Blaming the compiler for a slow editor. The editor and tsc run the same checker, so a type that costs a second in one will cost it in the other.

If you skip this, you will fix a slow build by buying a bigger runner, and the file that caused it will still be there. What is the Types number for your repository today, and who would notice if it doubled?

About

Measure a build with extendedDiagnostics, then try skipLibCheck, a type that costs too much and incremental, with the numbers from a real run. After this, you change a build setting because you measured, not because you guessed.

Duration
10 min
Last update
10/12/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.