🚀AdvancedExpert Patterns

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

The TypeScript Compiler API exposes the same machinery that `tsc` uses internally. You can programmatically parse TypeScript files, navigate the AST, check types, and emit output. This opens up a class of tools that are simply not possible with plain text manipulation. Common use cases: - **Code generation**: generate TypeScript type definitions from JSON Schema, OpenAPI, database schemas, or GraphQL SDL - **Codemods**: automated refactoring - rename a function and update all its call sites with full type awareness - **Custom linters**: rules beyond what ESLint can express, requiring type information (e.g., "this async function must have its return type annotated") - **Documentation generators**: extract JSDoc, type information, and examples from TypeScript source The two entry points are: 1. `ts.createProgram()` - the full compiler pipeline, good for type-checked analysis 2. `ts.createSourceFile()` - parse a single file to AST without type checking (faster, simpler) The API is in the `typescript` npm package (`import * as ts from 'typescript'`).

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: add

Using 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.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What is the difference between `ts.createSourceFile()` and `ts.createProgram()`?