TypeScript Compiler API
Parent: TypeScript Expert · researched 2026-06-03T23:22:50.256Z· 7 sources · 11 concepts · skill typescript-compiler-api
A lang-js-ts reference for driving the typescript npm package as a library — parsing source to
TypeScript Compiler API & Programmatic Tooling (Strada)
- A lang-js-ts reference for driving the typescript npm package as a library - parsing source to [source]
- an AST, type-checking through the TypeChecker, rewriting code with custom transformers built on [source]
- the modern ts.factory node API, hosting the Language Service, and the ts-morph wrapper that [source]
- makes all of it ergonomic. This is the engine behind linters, codemods, code generators, doc tools, [source]
- and editor plugins. For the type system and tsconfig defer to typescript-expert.md / [source]
- typescript-compiler-config; for running .ts (type stripping, tsx, ts-node) defer to [source]
- nodejs-typescript-and-runtime-features.md. [source]
Overview
- The compiler ships one public module (import * as ts from "typescript") exposing the same pipeline [source]
- tsc uses: a scanner/parser turns text into an immutable AST (ts.SourceFile of ts.Nodes); [source]
- a binder + TypeChecker resolve symbols and types; transformers rewrite the tree; a [source]
- printer/emitter writes .js/.d.ts. You opt into as much of that as you need - a one-file [source]
- syntactic codemod uses only the parser; a type-aware lint rule needs a full Program + checker. [source]
- The single most important framing fact - Strada vs Corsa: everything in this skill is the [source]
- "Strada" API, the original JavaScript/TypeScript-based compiler. **TS 6.0 (2026-03-23) is the [source]
- final JS-based release. TS 7.0 "Corsa" is a ground-up Go port (≈10× faster) that does not [source]
- support the Strada compiler API** - a replacement programmatic API is in progress and not stable as [source]
- of mid-2026. So any tool you write against this surface targets TS ≤ 6.x. ts-morph wraps Strada [source]
- too, so it shares that ceiling. Plan migrations accordingly; don't assume your transformer/LS plugin [source]
- Version anchors (memorize - these drive most "does this API exist" questions): [source]
- > Caveat: ts.createSourceFile survives because it is the parser entry point (text → tree), a [source]
- > different thing from the removed factory ts.createXxx node builders. Don't be misled by the name [source]
1. Two entry points: `createSourceFile` (parse only) vs `createProgram` (has a checker)
- ts.createSourceFile(fileName, text, langVersion, setParentNodes?) parses ONE in-memory string [source]
- into a SourceFile. No types, no cross-file resolution, no checker. This is all a syntactic [source]
- codemod/linter needs. Pass setParentNodes = true if you'll call node.getStart()/getText() [source]
- (they need parent pointers - see Concept 5). [source]
- ts.createProgram(rootFileNames, options, host?) builds a multi-file Program: it resolves [source]
- imports, runs the binder, and is the only way to get a TypeChecker via [source]
- program.getTypeChecker(). Use it for anything type-aware. options is a CompilerOptions [source]
- (defer the option semantics to typescript-compiler-config). [source]
2. The CompilerHost — controlling I/O
- createProgram's third arg is a CompilerHost: the abstraction the compiler uses to read files, [source]
- resolve modules, and write output. ts.createCompilerHost(options) gives the default disk-backed [source]
- host; override its methods to feed source from memory, a VFS, or a network, and to capture emit [source]
- output instead of writing to disk. [source]
3. Diagnostics
- ts.getPreEmitDiagnostics(program) → all syntactic + semantic + global errors before emit. [source]
- program.emit() returns an EmitResult whose .diagnostics are emit-time errors; combine via [source]
- ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics). [source]
- Format for humans: ts.formatDiagnosticsWithColorAndContext(diags, host) (ANSI, code frames) or [source]
- ts.formatDiagnostics(diags, host) (plain). For a single message string use [source]
- ts.flattenDiagnosticMessageText(d.messageText, "\n"). [source]
4. Emitting JS
- program.emit(targetSourceFile?, writeFile?, cancellationToken?, emitOnlyDtsFiles?, customTransformers?) [source]
- writes output through the host (or your writeFile callback). The 5th arg accepts [source]
- { before, after, afterDeclarations } transformer arrays - this is how you run a transformer [source]
- through the compiler (vs the standalone ts.transform, Concept 6). emitOnlyDtsFiles: true emits [source]
- declarations only - but declaration-emit semantics and hand-authoring .d.ts are out of scope → [source]
- typescript-declaration-files. [source]
5. The AST: nodes, kinds, walking, positions, trivia
- A ts.Node has a numeric kind (ts.SyntaxKind enum). Narrow with type guards: [source]
- ts.isFunctionDeclaration(node), ts.isCallExpression(node), ts.isIdentifier(node), etc. - these [source]
- give correct TS narrowing, far better than raw kind === checks. [source]
- Walking - two different traversals, a classic codemod trap: [source]
- ts.forEachChild(node, cb) visits only the semantically significant child nodes; it [source]
- skips tokens, punctuation, and trivia. Use it for analysis/codemods. Returning a truthy value [source]
- short-circuits (like Array.find). [source]
- node.getChildren(sourceFile?) returns every child including token nodes (braces, [source]
- commas, keywords). Heavier; needed when you care about punctuation. **Requires a parsed tree with [source]
- parent pointers** - throws on synthesized factory nodes. [source]
- Positions / text / trivia: node.getStart(sf) (start after leading trivia), node.pos (raw [source]
- start, includes leading trivia), node.end, node.getText(sf), node.getFullText(sf) (with [source]
- trivia), ts.getLeadingCommentRanges(fullText, node.pos) for comments. **All of these need a real [source]
- parsed SourceFile with setParentNodes/parents** - on ts.factory-created nodes pos/end are [source]
- -1 and these throw. [source]
6. The TypeChecker — resolving types and symbols
- The checker is where meaning lives. You must have a Program (a parse-only SourceFile has no [source]
- checker). Core methods: [source]
- checker.getTypeAtLocation(node) → the Type at any expression/decl node. [source]
- checker.getSymbolAtLocation(node) → the Symbol (declaration identity) for a name node. [source]
- checker.getTypeOfSymbolAtLocation(symbol, node) → a symbol's type in context (handles [source]
- overloads/locations). [source]
- checker.typeToString(type) → human-readable type text (e.g. (name: string) => string). [source]
- Signatures: type.getCallSignatures() → Signature[]; sig.getReturnType(), [source]
- sig.getParameters(). Symbols: symbol.getName(), symbol.valueDeclaration, [source]
- checker.getDeclaredTypeOfSymbol(sym), checker.getPropertiesOfType(type). [source]
7. Custom transformers with `ts.factory`
- A TransformerFactory<T> is (context: ts.TransformationContext) => (node: T) => T. Inside, you [source]
- recurse with ts.visitEachChild(node, visitor, context) (rewrites children) and return [source]
- replacement nodes built with ts.factory.createXxx - the AST is immutable, so you create new [source]
- nodes or update existing ones (ts.factory.updateXxx(original, ...newChildren) preserves position [source]
- and emit info - prefer it over create when editing in place). [source]
- Run a transformer two ways: [source]
- Standalone: ts.transform(sourceOrNodes, [transformer], options?) → TransformationResult; [source]
- print with ts.createPrinter().printNode(...) or printFile(...). Call result.dispose(). [source]
- Through emit: pass { before: [t] } as the 5th arg to program.emit(...). [source]
- before runs before TS's built-in transforms, after runs after them (on downleveled output), [source]
- afterDeclarations transforms the .d.ts tree. [source]
- > ts.visitNode visits a single node; ts.visitEachChild visits its children - you typically pair [source]
- > them (top-level visitNode, recursive visitEachChild). To synthesize entirely new code, compose [source]
- > ts.factory calls (e.g. ts.factory.createCallExpression(ts.factory.createIdentifier("log"), undefined, [arg])). [source]
8. Plugging transformers into a build — the `tsc` gap
- Vanilla tsc (the CLI) runs NO custom transformers. There is no tsconfig flag for it. Your [source]
- options, from most to least direct: [source]
- Programmatically - ts.transform or program.emit(…, { before, after }) (Concept 7). Full [source]
- control; you own the build script. [source]
- Build-tool integration - most loaders accept transformers: ts-loader [source]
- (options.getCustomTransformers), ts-jest, rollup-plugin-typescript2, etc. Bundlers using [source]
- esbuild/swc do not run TS transformers (different engine) → nodejs-build-tooling-bundlers. [source]
- Patch tsc - ts-patch (the maintained successor to the older ttypescript) adds a [source]
- plugins array under compilerOptions so tspc (its patched CLI) runs transformers during [source]
- a normal build. Keys: transform (module path, required), after, afterDeclarations, [source]
- transformProgram, import (named export), type. (Its persistent in-node_modules [source]
- patch mode is "more limited in TypeScript 6+"; prefer the live tspc/ts-patch/compiler path.) [source]
- > **Do not confuse this with the native compilerOptions.plugins array - that one is Language Service [source]
- > plugins only (Concept 9). Same JSON key, completely different mechanism.** [source]
9. The Language Service + tsserver LS plugins
- The Language Service (ts.createLanguageService(host, registry?)) is the incremental, editor [source]
- half of the compiler: it answers completions, quick-info (hover), diagnostics, go-to-definition, [source]
- rename, and refactors. You feed it a LanguageServiceHost - like a CompilerHost but built for [source]
- mutation: it must report file versions (bump the version string when a file changes) so the service [source]
- re-checks only what moved. [source]
- tsserver Language-Service plugins wrap this service to add editor features for everyone using the [source]
- project (e.g. a framework's template-aware completions). You ship a module exporting [source]
- function init({ typescript }) { return { create(info) { / wrap info.languageService / return proxy; } }; } [source]
- and register it in tsconfig's native compilerOptions.plugins: [source]
- These run inside the editor's tsserver, not in tsc builds - they change the dev experience, not [source]
10. ts-morph — the high-level wrapper
- ts-morph wraps the compiler API with a navigable, mutable object model so you skip the visitor/factory [source]
- boilerplate. Use it for navigation, refactoring, and codegen ergonomics; drop to the raw API only [source]
- when you need something it doesn't expose (then reach node.compilerNode for the underlying ts.Node, [source]
- and project.getTypeChecker().compilerObject for the raw checker). [source]
- new Project({ tsConfigFilePath }) (or useInMemoryFileSystem: true) is the root. [source]
- Load: project.addSourceFilesAtPaths("src/**/*.ts"), addSourceFileAtPath(p), [source]
- createSourceFile(path, text). [source]
- Navigate: sourceFile.getFunctionOrThrow("name"), getClasses(), [source]
- getDescendantsOfKind(SyntaxKind.CallExpression), node.getType().getText(). [source]
- Manipulate: fn.rename("sum") (project-wide rename!), cls.addMethod({...}), [source]
- node.replaceWithText(...), sourceFile.addImportDeclaration({...}). [source]
- Persist: await project.save() writes changed files back. [source]
- Raw API vs ts-morph: raw is leaner (no extra dep), exact, and what you need for build-time [source]
- transformers and LS plugins; ts-morph is faster to write for one-shot codemods, scaffolding/codegen, [source]
- and bulk renames. Both target Strada (TS ≤ 6.x). [source]
11. typescript-eslint `parserServices` (pointer only)
- For type-aware lint rules, @typescript-eslint/parser (with parserOptions.project) attaches [source]
- parserServices to each rule, exposing getTypeChecker() and [source]
- esTreeNodeToTSNodeMap/tsNodeToESTreeNodeMap to bridge the ESLint ESTree node to the TS Node and [source]
- its Type. That is the entry point for the whole typed-lint domain - **authoring those rules is out [source]
- of scope** → typescript-eslint-typed-linting. [source]
Tools / Frameworks
- typescript (the package) - ts.createProgram, ts.createSourceFile, ts.createCompilerHost, [source]
- program.getTypeChecker(), program.emit(), ts.transform, ts.createPrinter, ts.factory.*, [source]
- ts.createLanguageService. Strada API; TS ≤ 6.x. [source]
- ts-morph - high-level wrapper (Project, SourceFile, getDescendantsOfKind, rename, [source]
- save); bundles its own TS (6.0.2 in v28). [source]
- ts-patch (tspc) - successor to ttypescript; runs build transformers via [source]
- compilerOptions.plugins. [source]
- @typescript-eslint/parser parserServices - bridge to the checker for typed lint rules [source]
- (defer rule authoring). [source]
Methodology
- Pick the entry point by need. Syntactic-only (formatting, simple codemod) → createSourceFile [source]
- (set setParentNodes if you read positions). Anything type-aware → createProgram + checker. [source]
- Choose raw vs ts-morph. One-shot codemod / scaffolding / bulk rename → ts-morph. Build-time [source]
- transformer or LS plugin → raw API (no wrapper in the build path). [source]
- Walk with the right traversal. Analysis → forEachChild + ts.isXxx guards. Need tokens/punct [source]
- → getChildren (parsed tree only). [source]
- Mutate immutably. Build with ts.factory.createXxx; prefer ts.factory.updateXxx when editing [source]
- in place; recurse with visitEachChild; print with createPrinter. [source]
- Decide how it runs. Programmatic (ts.transform / emit), build-tool loader, or ts-patch. [source]
- Never expect vanilla tsc to run it. [source]
- Read diagnostics via getPreEmitDiagnostics (+ emit diagnostics); format with [source]
- formatDiagnosticsWithColorAndContext. [source]
- Mind the ceiling. This is Strada (TS ≤ 6.x); TS 7 "Corsa" won't run it - note that in any tool's [source]
Practical Patterns
- AST linter: parse-only createSourceFile → forEachChild + guards → collect [source]
- { file, line, message } from getLineAndCharacterOfPosition(node.getStart(sf)). [source]
- Type extractor / API surface: createProgram → checker → for each exported symbol [source]
- getTypeOfSymbolAtLocation + typeToString (and getCallSignatures) → dump JSON. [source]
- Codemod: TransformerFactory with ts.factory.updateXxx → ts.transform → printer.printFile [source]
- → write back; or ts-morph getDescendantsOfKind + replaceWithText + project.save(). [source]
- Codegen: assemble brand-new files from ts.factory nodes, or `project.createSourceFile(path, [source]
- templateText)` then refine via the model. [source]
- Build-plugged transform: author once, register under ts-patch compilerOptions.plugins [source]
- (transform/after), build with tspc. [source]
- Editor feature: LanguageServiceHost with versioned snapshots → createLanguageService → [source]
- getCompletionsAtPosition / getSemanticDiagnostics; ship as a tsserver plugin via native [source]
- compilerOptions.plugins. [source]
Anti-Patterns
- Calling ts.createIdentifier / ts.createCall / ts.createNode / ts.updateXxx - **removed in [source]
- TS 5.0** (verified undefined in 5.8.3 and 6.0.3). Always ts.factory.createXxx / updateXxx. [source]
- Mutating node fields in place - the AST is immutable; create or update nodes instead. [source]
- Reading getStart/getText/getChildren/positions on ts.factory-synthesized nodes (pos/end = [source]
- -1) - they throw; those need a parsed tree with parent pointers (setParentNodes). [source]
- Confusing forEachChild (named children, no tokens) with getChildren() (all tokens) - picking the [source]
- wrong one silently skips or floods nodes in a codemod. [source]
- Expecting vanilla tsc to run a transformer - it never does; use programmatic emit, a loader, or [source]
- Conflating the two plugins arrays: native compilerOptions.plugins = LS/editor plugins (tsc [source]
- ignores); ts-patch's compilerOptions.plugins (with transform) = build transformers. [source]
- Asking getTypeChecker() on a parse-only SourceFile - there's no checker without a Program. [source]
- Assuming compiler-API tooling (or ts-morph) survives the move to TS 7 "Corsa" - Strada API is [source]
- dropped; budget a rewrite. [source]
Troubleshooting
- ts.createXxx is not a function → removed in 5.0; switch to ts.factory.createXxx. [source]
- Cannot read properties of undefined (reading 'getStart'/'pos') or -1 positions → node is [source]
- synthesized (factory) or you parsed without setParentNodes; re-parse with parents or don't read [source]
- positions off synthetic nodes. [source]
- Checker returns any/undefined symbols → the file isn't in the Program's root set, or imports [source]
- didn't resolve (check CompilerOptions.module/moduleResolution, or a custom host's readFile). [source]
- Transformer "did nothing" under tsc → vanilla tsc ignores transformers; run via ts.transform, [source]
- program.emit({before}), a loader, or tspc. [source]
- LS plugin not loading → it only runs in the editor's tsserver, not tsc; confirm the editor uses the [source]
- workspace TS version and the plugin name resolves. [source]
- getCompletionsAtPosition stale after edits → bump the file's getScriptVersion string so the [source]
- Language Service invalidates its cache. [source]
References
- TS wiki - Using the Compiler API: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API [source]
- typescript package source / API (probed at runtime, 6.0.3 / 5.8.3 / 4.9.5): https://github.com/microsoft/TypeScript [source]
- ts-morph docs (bundles ts 6.0.2): https://ts-morph.com/ [source]
- ts-patch (successor to ttypescript; compilerOptions.plugins): https://github.com/nonara/ts-patch [source]
- MS DevBlogs - Progress on TypeScript 7 ("Corsa", Dec 2025): https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/ [source]
- VS Magazine - TS 6.0 ships as final JS-based release (2026-03-23): https://visualstudiomagazine.com/articles/2026/03/23/typescript-6-0-ships-as-final-javascript-based-release-clears-path-for-go-native-7-0.aspx [source]
- typescript-eslint - parserServices / typed linting: https://typescript-eslint.io/getting-started/typed-linting/ [source]
Children
- No children recorded.