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
SourceFileis an AST ofNodes; walk it withts.forEachChildand test kinds with guards likets.isFunctionDeclaration. program.getTypeChecker()answers semantic questions:checker.getTypeAtLocation(node),checker.typeToString(type),getSymbolAtLocation.ts.getPreEmitDiagnostics(program)returns the errorstscwould 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 --extendedDiagnosticsprints files, lines, instantiations, memory and time per phase. Huge instantiation counts point at expensive generic types.tsc --generateTrace outDirrecords a trace you can open in a Chromium performance viewer or summarise with@typescript/analyze-traceto find the hot files and types.--listFiles/--explainFilesreveal files you didn't expect to be included (a stray@typespackage, an over-broadinclude).
Common fixes:
- Prefer
interface ... extendsover large intersections; interfaces are cached by identity. - Add explicit return types on exported functions, so the checker (and
.d.tsemit) don't re-infer complex types. - Name complex types instead of repeating inline expressions; avoid giant unions and deep recursive types.
- Use
skipLibCheck,incrementaland 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 --extendedDiagnosticsRemember
- Program = files + options; TypeChecker answers semantic questions about AST nodes
- Measure with
--extendedDiagnosticsand--generateTracebefore 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.