Skip to main content

Overview

The Parser converts a stream of tokens from the Scanner into an Abstract Syntax Tree (AST). The AST is a tree representation of the source code structure that can be analyzed and transformed.

Creating Source Files

ts.createSourceFile()

Creates a SourceFile AST from source text.
fileName
string
required
The name of the file
sourceText
string
required
The source code text
languageVersionOrOptions
ScriptTarget | CreateSourceFileOptions
required
Either a script target or options object
setParentNodes
boolean
Whether to set parent pointers on nodes (defaults to false)
scriptKind
ScriptKind
The kind of script (TS, JS, JSX, etc.)
return
SourceFile
The parsed source file AST

Example

import * as ts from 'typescript';

// Simple usage
const sourceFile = ts.createSourceFile(
  'example.ts',
  'const x: number = 42;',
  ts.ScriptTarget.Latest,
  true // setParentNodes
);

// With options
const sourceFile2 = ts.createSourceFile(
  'example.tsx',
  'const component = <div>Hello</div>;',
  {
    languageVersion: ts.ScriptTarget.ES2020,
  },
  true,
  ts.ScriptKind.TSX
);

SourceFile Interface

The SourceFile represents the root of the AST and provides access to the parsed code structure.

Key Properties

interface SourceFile extends Node {
  fileName: string;              // The file name
  text: string;                  // The full source text
  statements: NodeArray<Statement>; // Top-level statements
  endOfFileToken: Token<SyntaxKind.EndOfFileToken>;
  
  // Helper methods
  getLineAndCharacterOfPosition(pos: number): LineAndCharacter;
  getPositionOfLineAndCharacter(line: number, character: number): number;
  getLineStarts(): readonly number[];
  getLineEndOfPosition(pos: number): number;
}

Example

const sourceFile = ts.createSourceFile(
  'example.ts',
  `const x = 1;
const y = 2;`,
  ts.ScriptTarget.Latest,
  true
);

console.log(`File: ${sourceFile.fileName}`);
console.log(`Statements: ${sourceFile.statements.length}`);

// Get line and character from position
const pos = 15; // position in the source
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
console.log(`Position ${pos} is at line ${line + 1}, column ${character + 1}`);

// Get position from line and character
const position = sourceFile.getPositionOfLineAndCharacter(1, 0);
console.log(`Line 2, column 1 starts at position ${position}`);

Working with the AST

Node Types

The AST consists of various node types, all extending the base Node interface:
interface Node {
  kind: SyntaxKind;              // The type of node
  pos: number;                   // Start position (includes trivia)
  end: number;                   // End position
  flags: NodeFlags;              // Node flags
  parent?: Node;                 // Parent node (if setParentNodes was true)
  
  // Methods
  getSourceFile(): SourceFile;
  getChildCount(sourceFile?: SourceFile): number;
  getChildAt(index: number, sourceFile?: SourceFile): Node;
  getChildren(sourceFile?: SourceFile): Node[];
  getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number;
  getFullStart(): number;
  getEnd(): number;
  getWidth(sourceFile?: SourceFile): number;
  getFullWidth(): number;
  getLeadingTriviaWidth(sourceFile?: SourceFile): number;
  getFullText(sourceFile?: SourceFile): string;
  getText(sourceFile?: SourceFile): string;
}

Traversing the AST

ts.forEachChild()

Visits each child node of a node.
node
Node
required
The node to visit children of
cbNode
(node: Node) => T | undefined
required
Callback for each child node
return
T | undefined
The first truthy value returned by the callback
function visit(node: ts.Node) {
  console.log(`Node: ${ts.SyntaxKind[node.kind]}`);
  
  ts.forEachChild(node, visit);
}

const sourceFile = ts.createSourceFile(
  'example.ts',
  'function add(a: number, b: number) { return a + b; }',
  ts.ScriptTarget.Latest,
  true
);

visit(sourceFile);

Recursive Visitor Pattern

function visitNode(node: ts.Node, depth: number = 0): void {
  const indent = '  '.repeat(depth);
  console.log(`${indent}${ts.SyntaxKind[node.kind]}`);
  
  // Visit all children recursively
  ts.forEachChild(node, (child) => visitNode(child, depth + 1));
}

const sourceFile = ts.createSourceFile(
  'example.ts',
  `
  class Person {
    constructor(public name: string) {}
    
    greet() {
      return "Hello, " + this.name;
    }
  }
  `,
  ts.ScriptTarget.Latest,
  true
);

visitNode(sourceFile);

Type Guards

TypeScript provides type guard functions to check node types:
import * as ts from 'typescript';

function analyzeNode(node: ts.Node) {
  if (ts.isVariableStatement(node)) {
    console.log('Variable statement');
  } else if (ts.isFunctionDeclaration(node)) {
    console.log('Function declaration');
    if (node.name) {
      console.log(`  Name: ${node.name.text}`);
    }
  } else if (ts.isClassDeclaration(node)) {
    console.log('Class declaration');
    if (node.name) {
      console.log(`  Name: ${node.name.text}`);
    }
  } else if (ts.isInterfaceDeclaration(node)) {
    console.log('Interface declaration');
  } else if (ts.isTypeAliasDeclaration(node)) {
    console.log('Type alias declaration');
  }
}
Common type guards:
// Declarations
ts.isFunctionDeclaration(node)
ts.isClassDeclaration(node)
ts.isInterfaceDeclaration(node)
ts.isVariableDeclaration(node)
ts.isMethodDeclaration(node)
ts.isPropertyDeclaration(node)
ts.isParameter(node)

// Statements
ts.isIfStatement(node)
ts.isForStatement(node)
ts.isWhileStatement(node)
ts.isReturnStatement(node)
ts.isExpressionStatement(node)
ts.isBlock(node)

// Expressions
ts.isBinaryExpression(node)
ts.isCallExpression(node)
ts.isPropertyAccessExpression(node)
ts.isElementAccessExpression(node)
ts.isIdentifier(node)
ts.isStringLiteral(node)
ts.isNumericLiteral(node)
ts.isArrowFunction(node)

// Types
ts.isTypeReferenceNode(node)
ts.isArrayTypeNode(node)
ts.isUnionTypeNode(node)
ts.isIntersectionTypeNode(node)
ts.isFunctionTypeNode(node)

Analyzing Specific Constructs

Analyzing Functions

import * as ts from 'typescript';

function analyzeFunctions(sourceFile: ts.SourceFile) {
  function visit(node: ts.Node) {
    if (ts.isFunctionDeclaration(node)) {
      console.log('\nFunction Declaration:');
      
      // Name
      if (node.name) {
        console.log(`  Name: ${node.name.text}`);
      }
      
      // Parameters
      console.log('  Parameters:');
      node.parameters.forEach(param => {
        const name = param.name.getText(sourceFile);
        const type = param.type ? param.type.getText(sourceFile) : 'any';
        console.log(`    ${name}: ${type}`);
      });
      
      // Return type
      if (node.type) {
        console.log(`  Return type: ${node.type.getText(sourceFile)}`);
      }
      
      // Modifiers
      if (node.modifiers) {
        const modifiers = node.modifiers
          .map(m => ts.SyntaxKind[m.kind])
          .join(', ');
        console.log(`  Modifiers: ${modifiers}`);
      }
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
}

const code = `
export async function fetchData(url: string, options?: RequestInit): Promise<Response> {
  return fetch(url, options);
}
`;

const sourceFile = ts.createSourceFile('example.ts', code, ts.ScriptTarget.Latest, true);
analyzeFunctions(sourceFile);

Analyzing Classes

import * as ts from 'typescript';

function analyzeClasses(sourceFile: ts.SourceFile) {
  function visit(node: ts.Node) {
    if (ts.isClassDeclaration(node)) {
      console.log('\nClass Declaration:');
      
      // Name
      if (node.name) {
        console.log(`  Name: ${node.name.text}`);
      }
      
      // Heritage (extends/implements)
      if (node.heritageClauses) {
        node.heritageClauses.forEach(clause => {
          const keyword = clause.token === ts.SyntaxKind.ExtendsKeyword 
            ? 'extends' 
            : 'implements';
          const types = clause.types
            .map(t => t.expression.getText(sourceFile))
            .join(', ');
          console.log(`  ${keyword}: ${types}`);
        });
      }
      
      // Members
      console.log('  Members:');
      node.members.forEach(member => {
        if (ts.isPropertyDeclaration(member)) {
          const name = member.name.getText(sourceFile);
          const type = member.type ? member.type.getText(sourceFile) : 'any';
          console.log(`    Property: ${name}: ${type}`);
        } else if (ts.isMethodDeclaration(member)) {
          const name = member.name.getText(sourceFile);
          console.log(`    Method: ${name}()`);
        } else if (ts.isConstructorDeclaration(member)) {
          console.log(`    Constructor`);
        }
      });
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
}

const code = `
class Animal {
  constructor(public name: string) {}
}

class Dog extends Animal {
  breed: string;
  
  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
  
  bark(): void {
    console.log('Woof!');
  }
}
`;

const sourceFile = ts.createSourceFile('example.ts', code, ts.ScriptTarget.Latest, true);
analyzeClasses(sourceFile);

Analyzing Imports and Exports

import * as ts from 'typescript';

function analyzeImportsExports(sourceFile: ts.SourceFile) {
  function visit(node: ts.Node) {
    if (ts.isImportDeclaration(node)) {
      console.log('\nImport:');
      
      // Module specifier
      const moduleSpecifier = node.moduleSpecifier;
      if (ts.isStringLiteral(moduleSpecifier)) {
        console.log(`  From: ${moduleSpecifier.text}`);
      }
      
      // Import clause
      if (node.importClause) {
        const clause = node.importClause;
        
        // Default import
        if (clause.name) {
          console.log(`  Default: ${clause.name.text}`);
        }
        
        // Named imports
        if (clause.namedBindings) {
          if (ts.isNamedImports(clause.namedBindings)) {
            const names = clause.namedBindings.elements
              .map(e => e.name.text)
              .join(', ');
            console.log(`  Named: { ${names} }`);
          } else if (ts.isNamespaceImport(clause.namedBindings)) {
            console.log(`  Namespace: * as ${clause.namedBindings.name.text}`);
          }
        }
      }
    } else if (ts.isExportDeclaration(node)) {
      console.log('\nExport:');
      
      if (node.exportClause && ts.isNamedExports(node.exportClause)) {
        const names = node.exportClause.elements
          .map(e => e.name.text)
          .join(', ');
        console.log(`  Named: { ${names} }`);
      }
      
      if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
        console.log(`  From: ${node.moduleSpecifier.text}`);
      }
    } else if (ts.isExportAssignment(node)) {
      console.log('\nExport default');
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
}

const code = `
import fs from 'fs';
import { readFile, writeFile } from 'fs/promises';
import * as path from 'path';

export { readFile };
export default class MyClass {}
`;

const sourceFile = ts.createSourceFile('example.ts', code, ts.ScriptTarget.Latest, true);
analyzeImportsExports(sourceFile);

Node Position Information

import * as ts from 'typescript';

function showPositions(node: ts.Node, sourceFile: ts.SourceFile) {
  // Full start includes leading trivia (whitespace, comments)
  const fullStart = node.getFullStart();
  
  // Start excludes leading trivia
  const start = node.getStart(sourceFile);
  
  // End position
  const end = node.getEnd();
  
  // Widths
  const fullWidth = node.getFullWidth();
  const width = node.getWidth(sourceFile);
  const leadingTriviaWidth = node.getLeadingTriviaWidth(sourceFile);
  
  // Text
  const fullText = node.getFullText(sourceFile);
  const text = node.getText(sourceFile);
  
  console.log('Position info:');
  console.log(`  Full range: ${fullStart} - ${end}`);
  console.log(`  Text range: ${start} - ${end}`);
  console.log(`  Leading trivia: ${leadingTriviaWidth} chars`);
  console.log(`  Full text: "${fullText}"`);
  console.log(`  Text: "${text}"`);
  
  // Line and character
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
  console.log(`  Location: line ${line + 1}, column ${character + 1}`);
}

Complete Example: Code Analyzer

import * as ts from 'typescript';

interface CodeStats {
  functions: number;
  classes: number;
  interfaces: number;
  variables: number;
  imports: number;
  exports: number;
  lines: number;
}

function analyzeCode(fileName: string, sourceText: string): CodeStats {
  const sourceFile = ts.createSourceFile(
    fileName,
    sourceText,
    ts.ScriptTarget.Latest,
    true
  );
  
  const stats: CodeStats = {
    functions: 0,
    classes: 0,
    interfaces: 0,
    variables: 0,
    imports: 0,
    exports: 0,
    lines: sourceFile.getLineStarts().length
  };
  
  function visit(node: ts.Node) {
    if (ts.isFunctionDeclaration(node)) {
      stats.functions++;
    } else if (ts.isClassDeclaration(node)) {
      stats.classes++;
    } else if (ts.isInterfaceDeclaration(node)) {
      stats.interfaces++;
    } else if (ts.isVariableStatement(node)) {
      node.declarationList.declarations.forEach(() => {
        stats.variables++;
      });
    } else if (ts.isImportDeclaration(node)) {
      stats.imports++;
    } else if (ts.isExportDeclaration(node) || ts.isExportAssignment(node)) {
      stats.exports++;
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
  
  return stats;
}

// Example usage
const code = `
import { Component } from 'react';

interface Props {
  name: string;
}

class MyComponent extends Component<Props> {
  render() {
    return <div>{this.props.name}</div>;
  }
}

export default MyComponent;
`;

const stats = analyzeCode('component.tsx', code);
console.log('Code Statistics:');
console.log(`  Lines: ${stats.lines}`);
console.log(`  Functions: ${stats.functions}`);
console.log(`  Classes: ${stats.classes}`);
console.log(`  Interfaces: ${stats.interfaces}`);
console.log(`  Variables: ${stats.variables}`);
console.log(`  Imports: ${stats.imports}`);
console.log(`  Exports: ${stats.exports}`);

See Also

Build docs developers (and LLMs) love