TypeScript

TypeScript Compiler API

42 min Lesson 25 of 40

TypeScript Compiler API

The TypeScript Compiler API provides programmatic access to the TypeScript compiler, allowing you to parse, analyze, transform, and generate TypeScript code. This powerful API enables you to build custom code transformers, linters, code generators, and development tools. In this lesson, we'll explore the Abstract Syntax Tree (AST), create custom transformers, generate code, and perform type checking programmatically.

Understanding the TypeScript AST

The Abstract Syntax Tree (AST) is a tree representation of your source code. Every piece of TypeScript code can be represented as nodes in this tree:

// Install TypeScript as a dependency npm install typescript // basic-ast.ts import * as ts from 'typescript'; // Parse source code into an AST const sourceCode = \` const greeting: string = "Hello, World!"; function add(a: number, b: number): number { return a + b; } \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); // Traverse the AST function visit(node: ts.Node, depth: number = 0): void { const indent = ' '.repeat(depth); console.log(\`${indent}${ts.SyntaxKind[node.kind]}\`); ts.forEachChild(node, (child) => visit(child, depth + 1)); } console.log('AST Structure:'); visit(sourceFile); // Output: // SourceFile // VariableStatement // VariableDeclarationList // VariableDeclaration // Identifier // StringKeyword // StringLiteral // FunctionDeclaration // Identifier // Parameter // Identifier // NumberKeyword // Parameter // Identifier // NumberKeyword // NumberKeyword // Block // ReturnStatement // BinaryExpression // Identifier // PlusToken // Identifier
Note: The AST represents your code as a hierarchical tree of nodes. Each node has a kind property that identifies what type of syntax element it represents.

Finding and Filtering Nodes

You can search the AST for specific types of nodes:

import * as ts from 'typescript'; // Helper to find nodes by type function findNodes<T extends ts.Node>( node: ts.Node, kind: ts.SyntaxKind ): T[] { const results: T[] = []; function visit(node: ts.Node): void { if (node.kind === kind) { results.push(node as T); } ts.forEachChild(node, visit); } visit(node); return results; } const sourceCode = \` const x = 10; let y = 20; var z = 30; function foo() {} const bar = () => {}; \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); // Find all variable declarations const variables = findNodes<ts.VariableDeclaration>( sourceFile, ts.SyntaxKind.VariableDeclaration ); console.log('Variable names:'); variables.forEach(v => { if (ts.isIdentifier(v.name)) { console.log(\` ${v.name.text}\`); } }); // Find all function declarations const functions = findNodes<ts.FunctionDeclaration>( sourceFile, ts.SyntaxKind.FunctionDeclaration ); console.log('\nFunction names:'); functions.forEach(f => { if (f.name) { console.log(\` ${f.name.text}\`); } }); // Advanced filtering: Find all const declarations function findConstDeclarations(node: ts.Node): ts.VariableDeclaration[] { const results: ts.VariableDeclaration[] = []; function visit(node: ts.Node): void { if (ts.isVariableStatement(node)) { const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0; if (isConst) { node.declarationList.declarations.forEach(decl => { results.push(decl); }); } } ts.forEachChild(node, visit); } visit(node); return results; } const constDeclarations = findConstDeclarations(sourceFile); console.log('\nConst declarations:'); constDeclarations.forEach(decl => { if (ts.isIdentifier(decl.name)) { console.log(\` ${decl.name.text}\`); } });

Creating Custom Transformers

Transformers allow you to modify the AST before code generation. This is useful for code optimization, adding features, or enforcing patterns:

import * as ts from 'typescript'; // Transform all console.log calls to console.debug function createConsoleTransformer(): ts.TransformerFactory<ts.SourceFile> { return (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { // Check if this is console.log if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'console' && node.expression.name.text === 'log' ) { // Replace 'log' with 'debug' return ts.factory.updateCallExpression( node, ts.factory.createPropertyAccessExpression( node.expression.expression, 'debug' ), node.typeArguments, node.arguments ); } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }; }; } // Apply the transformer const sourceCode = \` console.log("Hello"); console.log("World"); console.error("Error"); \`; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true ); const result = ts.transform(sourceFile, [createConsoleTransformer()]); const printer = ts.createPrinter(); const transformedCode = printer.printFile(result.transformed[0]); console.log('Transformed code:'); console.log(transformedCode); // Output: // console.debug("Hello"); // console.debug("World"); // console.error("Error"); result.dispose(); // More advanced transformer: Add logging to all functions function createFunctionLoggerTransformer(): ts.TransformerFactory<ts.SourceFile> { return (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { if (ts.isFunctionDeclaration(node) && node.body) { const functionName = node.name?.text || 'anonymous'; // Create console.log statement const logStatement = ts.factory.createExpressionStatement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier('console'), 'log' ), undefined, [ts.factory.createStringLiteral(\`Entering function: ${functionName}\`)] ) ); // Add log statement at the beginning of the function const newBody = ts.factory.updateBlock(node.body, [ logStatement, ...node.body.statements ]); return ts.factory.updateFunctionDeclaration( node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, newBody ); } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }; }; }
Tip: Use the ts.factory API to create new AST nodes. This ensures that the nodes are properly structured and compatible with the TypeScript compiler.

Code Generation

You can generate TypeScript code programmatically using the factory API:

import * as ts from 'typescript'; // Generate a simple class function generateUserClass(): ts.ClassDeclaration { // Create properties const idProperty = ts.factory.createPropertyDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], 'id', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), undefined ); const nameProperty = ts.factory.createPropertyDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], 'name', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), undefined ); // Create constructor const constructor = ts.factory.createConstructorDeclaration( undefined, [ ts.factory.createParameterDeclaration( undefined, undefined, 'id', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword) ), ts.factory.createParameterDeclaration( undefined, undefined, 'name', undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) ) ], ts.factory.createBlock([ ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'id' ), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier('id') ) ), ts.factory.createExpressionStatement( ts.factory.createBinaryExpression( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'name' ), ts.SyntaxKind.EqualsToken, ts.factory.createIdentifier('name') ) ) ]) ); // Create method const greetMethod = ts.factory.createMethodDeclaration( [ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)], undefined, 'greet', undefined, undefined, [], ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ts.factory.createBlock([ ts.factory.createReturnStatement( ts.factory.createTemplateExpression( ts.factory.createTemplateHead('Hello, my name is '), [ ts.factory.createTemplateSpan( ts.factory.createPropertyAccessExpression( ts.factory.createThis(), 'name' ), ts.factory.createTemplateTail('') ) ] ) ) ]) ); // Create the class return ts.factory.createClassDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'User', undefined, undefined, [idProperty, nameProperty, constructor, greetMethod] ); } // Print the generated code const userClass = generateUserClass(); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const sourceFile = ts.createSourceFile( 'generated.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS ); const result = printer.printNode( ts.EmitHint.Unspecified, userClass, sourceFile ); console.log('Generated class:'); console.log(result); // Output: // export class User { // public id: number; // public name: string; // constructor(id: number, name: string) { // this.id = id; // this.name = name; // } // public greet(): string { // return \`Hello, my name is ${this.name}\`; // } // }

Type Checking Programmatically

The Compiler API allows you to perform type checking on code without emitting files:

import * as ts from 'typescript'; import * as path from 'path'; // Create a program for type checking function createProgram(fileNames: string[], options: ts.CompilerOptions): ts.Program { const host = ts.createCompilerHost(options); return ts.createProgram(fileNames, options, host); } // Type check a file function typeCheck(fileName: string): void { const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, strict: true, noEmit: true }; const program = createProgram([fileName], options); const diagnostics = ts.getPreEmitDiagnostics(program); if (diagnostics.length === 0) { console.log('✓ No type errors found'); return; } console.log(\`✗ Found ${diagnostics.length} type error(s):\`); diagnostics.forEach(diagnostic => { if (diagnostic.file) { const { line, character } = ts.getLineAndCharacterOfPosition( diagnostic.file, diagnostic.start! ); const message = ts.flattenDiagnosticMessageText( diagnostic.messageText, '\n' ); console.log( \` ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}\` ); } else { console.log( \` ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}\` ); } }); } // Type check with custom type information function analyzeTypes(sourceCode: string): void { const sourceFile = ts.createSourceFile( 'temp.ts', sourceCode, ts.ScriptTarget.Latest, true ); const options: ts.CompilerOptions = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS }; const host = ts.createCompilerHost(options); const originalGetSourceFile = host.getSourceFile; host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { if (fileName === 'temp.ts') { return sourceFile; } return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); }; const program = ts.createProgram(['temp.ts'], options, host); const checker = program.getTypeChecker(); function visit(node: ts.Node): void { if (ts.isVariableDeclaration(node) && node.initializer) { const type = checker.getTypeAtLocation(node.initializer); const typeName = checker.typeToString(type); if (ts.isIdentifier(node.name)) { console.log(\`Variable '${node.name.text}' has type: ${typeName}\`); } } ts.forEachChild(node, visit); } visit(sourceFile); } // Example usage const code = \` const name = "John"; const age = 30; const isActive = true; const items = [1, 2, 3]; const user = { name: "Jane", age: 25 }; \`; console.log('Type Analysis:'); analyzeTypes(code); // Output: // Variable 'name' has type: "John" // Variable 'age' has type: 30 // Variable 'isActive' has type: true // Variable 'items' has type: number[] // Variable 'user' has type: { name: string; age: number; }
Note: The TypeChecker is the most powerful part of the Compiler API. It provides access to all type information and can answer questions about types, symbols, and their relationships.

Building a Simple Linter

Combine AST traversal and type checking to build custom linting rules:

import * as ts from 'typescript'; interface LintError { line: number; column: number; message: string; } // Rule: Disallow 'any' type function checkForAnyType(sourceFile: ts.SourceFile): LintError[] { const errors: LintError[] = []; function visit(node: ts.Node): void { // Check for explicit 'any' type annotations if (node.kind === ts.SyntaxKind.AnyKeyword) { const { line, character } = sourceFile.getLineAndCharacterOfPosition( node.getStart() ); errors.push({ line: line + 1, column: character + 1, message: 'Usage of \'any\' type is not allowed' }); } ts.forEachChild(node, visit); } visit(sourceFile); return errors; } // Rule: Require explicit return types for functions function checkFunctionReturnTypes(sourceFile: ts.SourceFile): LintError[] { const errors: LintError[] = []; function visit(node: ts.Node): void { if ( (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && !node.type ) { const { line, character } = sourceFile.getLineAndCharacterOfPosition( node.getStart() ); const name = node.name ? node.name.getText() : 'anonymous'; errors.push({ line: line + 1, column: character + 1, message: \`Function '${name}' is missing return type annotation' }); } ts.forEachChild(node, visit); } visit(sourceFile); return errors; } // Run all linting rules function lint(sourceCode: string): void { const sourceFile = ts.createSourceFile( 'test.ts', sourceCode, ts.ScriptTarget.Latest, true ); const anyTypeErrors = checkForAnyType(sourceFile); const returnTypeErrors = checkFunctionReturnTypes(sourceFile); const allErrors = [...anyTypeErrors, ...returnTypeErrors]; if (allErrors.length === 0) { console.log('✓ No linting errors found'); return; } console.log(\`✗ Found ${allErrors.length} linting error(s):\`); allErrors.forEach(error => { console.log(\` Line ${error.line}:${error.column} - ${error.message}\`); }); } // Test the linter const testCode = \` function greet(name: any) { return "Hello " + name; } function calculate(a: number, b: number) { return a + b; } \`; lint(testCode);
Warning: Working with the Compiler API can be complex. Always refer to the official TypeScript documentation and study existing tools (like ts-morph) that wrap the Compiler API with easier-to-use interfaces.
Exercise:
  1. Build a transformer that converts all arrow functions to regular function declarations.
  2. Create a code generator that takes a JSON schema and generates TypeScript interfaces.
  3. Implement a custom linter rule that enforces naming conventions (e.g., interfaces must start with 'I').
  4. Build a tool that analyzes a TypeScript codebase and generates a dependency graph showing which files import which.
  5. Create a transformer that automatically adds JSDoc comments to all exported functions based on their type signatures.

Summary

The TypeScript Compiler API is a powerful tool for building custom development tools. By understanding the AST, you can traverse and analyze code structure. Custom transformers allow you to modify code programmatically. The factory API enables code generation, and the type checker provides deep insights into type information. With these capabilities, you can build linters, code generators, refactoring tools, and much more, extending TypeScript to fit your specific needs.