Alright, let’s get our hands dirty. You’ve got a TypeScript program in memory, parsed into an Abstract Syntax Tree (AST). It’s a beautiful, terrifying, and deeply nested structure of objects. Your job is to traverse it, find the bits you care about, and do something useful. This isn’t about reading the file as text; it’s about understanding its meaning programmatically. To do that, you need to know two things intimately: ts.Node and ts.SyntaxKind.

Think of it like this: the AST is a sprawling city. A ts.Node is every single address—every building, every stop sign, every park bench. ts.SyntaxKind is the type of each address. Is 123 Main St a SingleFamilyHome, a Skyscraper, or a QuestionableAlleyway? You need both pieces of information to navigate effectively.

The ts.Node: Your Universal Interface

Every single meaningful part of your code is represented by a ts.Node. A variable declaration, a binary expression, a string literal, even a stray comma—they’re all nodes. The beautiful part is that they all share a common interface. You don’t need to know the specific type of node (like VariableDeclaration or BinaryExpression) to start traversing. The core ts.Node interface gives you the keys to the city:

  • kind: ts.SyntaxKind: The type of node this is. This is your most important property.
  • parent: ts.Node: Who owns this node. A VariableDeclaration’s parent is a VariableDeclarationList, whose parent is a VariableStatement. This is your way back up the tree.
  • getChildren(): ts.Node[]: A quick way to get all direct children of this node. It’s handy, but a bit blunt.
  • getChildAt(index: number): ts.Node: For when you need surgical precision.

Here’s the thing: the ts.Node is just the base. To actually do anything useful, you’ll almost always check its kind and then narrow it to a more specific type. The compiler API provides a whole zoo of these specific interfaces (isIdentifier, isFunctionDeclaration, etc.).

import * as ts from "typescript";

// Assume we have a sourceFile from ts.createSourceFile

function analyzeNode(node: ts.Node) {
  // First, check what we're dealing with using its kind
  console.log(`Node Kind: ${ts.SyntaxKind[node.kind]}`);

  // Is this a specific type of node? Let's check for a function.
  if (ts.isFunctionDeclaration(node)) {
    // Now, inside this block, 'node' is narrowed to ts.FunctionDeclaration
    // We can safely access FunctionDeclaration-specific properties!
    console.log(`Found function: ${node.name?.getText()}`);
  }

  // Then, recurse into its children to traverse the whole tree
  node.forEachChild(child => analyzeNode(child));
}

analyzeNode(sourceFile);

The ts.SyntaxKind Enum: Naming the Nouns

ts.SyntaxKind is a massive enum that gives a unique number to every single type of syntax construct in TypeScript. Trying to remember the number for IfStatement is a fool’s errand. Instead, you’ll almost always reference it via the enum object itself: ts.SyntaxKind.IfStatement.

This is where the first “questionable choice” rears its head. The enum is a mix of punctuation, keywords, and constructs. ts.SyntaxKind.FirstAssignment and ts.SyntaxKind.FirstLiteralToken live right next to ts.SyntaxKind.OpenBraceToken and ts.SyntaxKind.EndOfFileToken. It’s… eclectic. You don’t need to memorize it, you just need to know how to find what you need (pro tip: search the typescript.d.ts file for what you’re looking for, like InterfaceDeclaration).

The most common pitfall here is assuming a node’s kind will tell you everything. It won’t. It tells you the type of the node, but not its content. You know you have a BinaryExpression (kind 101), but you still have to check its operatorToken to see if it’s a + or a -.

How to Actually Walk the Tree: forEachChild vs getChildren

You have two primary methods for traversing down the tree. They serve different purposes.

  • node.forEachChild(child => { ... }): This is your best friend 95% of the time. It’s efficient and it’s smart. It doesn’t waste your time with trivia nodes like whitespace or comments by default (unless you specifically ask the parser to include them). It gives you the meaningful, logical structure of the code. Use this for almost every traversal.

  • node.getChildren(): This is the blunt instrument. It returns every child node the parser created, including all the trivia. The array might be full of WhitespaceTrivia, EndOfLineToken, and other syntactic fluff you usually don’t care about. It’s useful only in rare edge cases where you need absolute fidelity to the original source text, punctuation and all. Avoid this unless you have a very specific reason.

The Power of Narrowing: ts.is* Functions

The TypeScript team didn’t leave you to cast nodes blindly with as. They provide a gorgeous suite of type guard functions. You’ve already seen ts.isFunctionDeclaration(node). There’s one for almost every SyntaxKind: ts.isVariableStatement, ts.isIfStatement, ts.isStringLiteral, etc.

These functions are your bread and butter. They check the kind property and narrow the type in a single, clean operation. This is infinitely safer and more readable than manually checking node.kind === ts.SyntaxKind.IfStatement.

function visit(node: ts.Node, depth: number = 0) {
  const indent = '  '.repeat(depth);

  // Use type guards to narrow the node type and act accordingly
  if (ts.isIfStatement(node)) {
    console.log(`${indent}Found an if statement condition:`);
    // 'node' is now an IfStatement, so we can access .expression
    visit(node.expression, depth + 1);
  } else if (ts.isBinaryExpression(node)) {
    // Let's get fancy and check the operator
    const operator = node.operatorToken.kind;
    if (operator === ts.SyntaxKind.PlusToken) {
      console.log(`${indent}Found a PLUS operation`);
    } else if (operator === ts.SyntaxKind.EqualsToken) {
      console.log(`${indent}Found an ASSIGNMENT (the real kind)`);
    }
    visit(node.left, depth + 1);
    visit(node.right, depth + 1);
  } else {
    // For any other node, just recurse into its children
    node.forEachChild(child => visit(child, depth));
  }
}

The key takeaway? Always start with the kind or a type guard. Know what you’re looking at before you try to touch its specific properties. It’s the difference between politely asking a building for its business hours and walking into a stop sign expecting a latte.