TypeScript Compiler API
Use the TypeScript Compiler API to build code generators, linters, codemods, and documentation tools that work directly with the TypeScript AST.
Overview
Key Points
- •`ts.createSourceFile(filename, code, target)` - parse a string of TS code into an AST
- •`ts.createProgram(files, options)` - full program with type checker access
- •`program.getTypeChecker()` - returns the TypeChecker for resolving types at nodes
- •AST nodes: `ts.SyntaxKind` enum names each node kind (FunctionDeclaration, ClassDeclaration, etc.)
- •`ts.forEachChild(node, visitor)` and `ts.visitEachChild(node, visitor, context)` traverse the AST
- •Node factory: `ts.factory.createXxx()` builds new AST nodes for code generation
- •`ts.transform(source, [transformer])` applies transformations to produce a new AST
Examples
Parsing a TypeScript file and visiting the AST
import * as ts from 'typescript';
// Parse a string of TypeScript code into an AST
const code = `
function greet(name: string): string {
return "Hello, " + name;
}
const add = (a: number, b: number): number => a + b;
`;
const sourceFile = ts.createSourceFile(
'example.ts', // filename (used for error messages)
code, // source text
ts.ScriptTarget.Latest,
true // setParentNodes - required for navigation
);
// Visitor function - called for each node
function visit(node: ts.Node): void {
if (ts.isFunctionDeclaration(node)) {
const name = node.name?.getText(sourceFile) ?? '<anonymous>';
const params = node.parameters.map(p => p.name.getText(sourceFile));
console.log(`Function: ${name}(${params.join(', ')})`);
}
if (ts.isArrowFunction(node)) {
const parent = node.parent;
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
console.log(`Arrow function: ${parent.name.text}`);
}
}
ts.forEachChild(node, visit); // Recurse into children
}
visit(sourceFile);
// Output:
// Function: greet(name)
// Arrow function: addUsing the type checker for type-aware analysis
import * as ts from 'typescript';
import * as fs from 'fs';
// Create a full program with type checking
const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
const configFile = ts.readConfigFile(configPath!, ts.sys.readFile);
const parsedCmd = ts.parseJsonConfigFileContent(configFile.config, ts.sys, './');
const program = ts.createProgram(parsedCmd.fileNames, parsedCmd.options);
const checker = program.getTypeChecker();
// Find all functions with implicit 'any' return types
for (const sourceFile of program.getSourceFiles()) {
// Skip node_modules
if (sourceFile.fileName.includes('node_modules')) continue;
function visitFn(node: ts.Node): void {
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
// Check if return type is explicitly annotated
if (!node.type) {
const signature = checker.getSignatureFromDeclaration(node);
if (signature) {
const returnType = checker.getReturnTypeOfSignature(signature);
const typeName = checker.typeToString(returnType);
const name = (node as ts.FunctionDeclaration).name?.text ?? '<anonymous>';
if (typeName === 'any') {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
console.log(`Warning: ${sourceFile.fileName}:${line + 1} - ${name}() returns any`);
}
}
}
}
ts.forEachChild(node, visitFn);
}
visitFn(sourceFile);
}Code generation with the node factory
import * as ts from 'typescript';
// Generate TypeScript interfaces from a schema
interface SchemaField {
name: string;
type: 'string' | 'number' | 'boolean' | 'Date';
optional?: boolean;
}
interface Schema {
name: string;
fields: SchemaField[];
}
function generateInterface(schema: Schema): ts.InterfaceDeclaration {
const { factory } = ts;
const members = schema.fields.map(field => {
const typeNode = factory.createKeywordTypeNode(
field.type === 'string' ? ts.SyntaxKind.StringKeyword :
field.type === 'number' ? ts.SyntaxKind.NumberKeyword :
field.type === 'boolean' ? ts.SyntaxKind.BooleanKeyword :
ts.SyntaxKind.ObjectKeyword // Date maps to object for simplicity
);
return factory.createPropertySignature(
undefined,
factory.createIdentifier(field.name),
field.optional ? factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
typeNode
);
});
return factory.createInterfaceDeclaration(
[factory.createToken(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier(schema.name),
undefined, // type parameters
undefined, // heritage clauses
members
);
}
// Emit the generated AST to a string
function emitNode(node: ts.Node): string {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const dummy = ts.createSourceFile('dummy.ts', '', ts.ScriptTarget.Latest);
return printer.printNode(ts.EmitHint.Unspecified, node, dummy);
}
const userSchema: Schema = {
name: 'User',
fields: [
{ name: 'id', type: 'string' },
{ name: 'name', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'age', type: 'number', optional: true },
],
};
const generated = generateInterface(userSchema);
console.log(emitNode(generated));
// Output:
// export interface User {
// id: string;
// name: string;
// email: string;
// age?: number;
// }Common Mistakes
- Using `node.getText()` without passing `sourceFile` as a parameter: without the source file, `getText()` cannot find the original source text and returns undefined or throws
- Checking `node.kind === ts.SyntaxKind.FunctionDeclaration` directly: prefer the type guard helpers like `ts.isFunctionDeclaration(node)` which also narrow the TypeScript type so you get proper autocomplete on the node properties
- Forgetting `setParentNodes: true` in `createSourceFile`: without this, `node.parent` is undefined, and many traversal patterns that navigate upward will fail silently or throw
- Mutating AST nodes directly: the TypeScript Compiler API uses immutable nodes. To modify the tree, use `ts.transform` with a transformer that creates new nodes via `ts.factory.createXxx()` and returns them from the visitor
Note
The TypeScript Compiler API has sparse official documentation, but the best resources are: (1) the TypeScript source code itself on GitHub - search for examples of the pattern you want, (2) the `ts-morph` library which wraps the raw API in a more ergonomic interface, and (3) the TypeScript Playground's AST viewer (paste code, enable "Show AST" in the plugins panel) to understand the node structure of any code you want to process.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4