Compiler API and type-checking performance

TypeScript · Expert · 18 min

Large codebases live or die by check times, and custom tooling (codegen, lint rules, migrations) is built on the compiler API.

The typescript package is also a library. The compiler API exposes the same machinery tsc and editors use:

  • ts.createProgram(files, options) builds a Program — the parsed source files plus options.
  • Each SourceFile is an AST of Nodes; walk it with ts.forEachChild and test kinds with guards like ts.isFunctionDeclaration.
  • program.getTypeChecker() answers semantic questions: checker.getTypeAtLocation(node), checker.typeToString(type), getSymbolAtLocation.
  • ts.getPreEmitDiagnostics(program) returns the errors tsc would print.

typescript-eslint rules, codegen tools and codemods are built this way (often through wrappers such as ts-morph). Note that Microsoft is porting the compiler to native code in Go (TypeScript 7, previewed as tsgo), which is many times faster but comes with a new API, so tools relying on the JavaScript API will need to adapt over time.

Performance. When checks get slow, measure first:

  • tsc --extendedDiagnostics prints files, lines, instantiations, memory and time per phase. Huge instantiation counts point at expensive generic types.
  • tsc --generateTrace outDir records a trace you can open in a Chromium performance viewer or summarise with @typescript/analyze-trace to find the hot files and types.
  • --listFiles / --explainFiles reveal files you didn't expect to be included (a stray @types package, an over-broad include).

Common fixes:

  • Prefer interface ... extends over large intersections; interfaces are cached by identity.
  • Add explicit return types on exported functions, so the checker (and .d.ts emit) don't re-infer complex types.
  • Name complex types instead of repeating inline expressions; avoid giant unions and deep recursive types.
  • Use skipLibCheck, incremental and project references to avoid redundant work.

Example

// count-any.ts: report every exported function whose return type is `any`
import ts from "typescript";

const program = ts.createProgram(["src/index.ts"], { strict: true, noEmit: true });
const checker = program.getTypeChecker();

for (const file of program.getSourceFiles()) {
  if (file.isDeclarationFile) continue;
  const visit = (node: ts.Node): void => {
    if (ts.isFunctionDeclaration(node) && node.name) {
      const signature = checker.getSignatureFromDeclaration(node);
      const returns = signature && checker.getReturnTypeOfSignature(signature);
      if (returns && returns.flags & ts.TypeFlags.Any) {
        const { line } = file.getLineAndCharacterOfPosition(node.getStart());
        console.log(`${file.fileName}:${line + 1} ${node.name.text} returns any`);
      }
    }
    ts.forEachChild(node, visit);
  };
  visit(file);
}

const errors = ts.getPreEmitDiagnostics(program);
console.log(`${errors.length} diagnostics`);
// Measure checker cost:  npx tsc --noEmit --extendedDiagnostics

Remember

  • Program = files + options; TypeChecker answers semantic questions about AST nodes
  • Measure with --extendedDiagnostics and --generateTrace before optimising
  • Interfaces, named types and explicit return types keep checks fast

Watch out for

  • Guessing at slow builds instead of checking instantiation counts and traces.

Check yourself

  1. Your build's --extendedDiagnostics shows tens of millions of instantiations. What is the likely cause?
  2. Which API call gives semantic information such as the type of an expression?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: github.com/microsoft/TypeScript/wiki/Performance

Compiler API and type-checking performance · TypeScript · Topolearn