CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ts-api-utils

Utility functions for working with TypeScript's API, providing comprehensive tools for analyzing and manipulating TypeScript AST nodes, types, and compiler APIs.

79

1.97x
Overview
Eval results
Files

syntax-utilities.mddocs/

Syntax Utilities

Syntax Utilities provide a comprehensive set of functions for working with TypeScript's syntax elements, including comments processing, flag utilities, modifier checks, scope boundary detection, and token manipulation. These utilities form the foundation for many advanced TypeScript analysis tasks.

Overview

TypeScript's syntax is rich and complex, with many nuanced rules governing how different language constructs behave. The Syntax Utilities module provides essential building blocks for:

  1. Comment Processing: Iterating over and analyzing code comments
  2. Flag Utilities: Testing various TypeScript flags on nodes, types, and symbols
  3. Modifier Utilities: Working with modifier tokens and syntax kinds
  4. Scope Utilities: Determining scope boundaries for variables and functions
  5. Syntax Validation: Checking syntax validity and properties
  6. Token Processing: Iterating over and manipulating syntax tokens

These utilities are essential for building sophisticated code analysis tools, formatters, and transformers that need to understand the structure and meaning of TypeScript code at a granular level.

Comments Processing

Comments are an important part of code documentation and can contain significant information for analysis tools.

Types

ForEachCommentCallback

type ForEachCommentCallback = (fullText: string, comment: ts.CommentRange) => void

Callback type used with forEachComment to process each comment found in the source code.

Parameters:

  • fullText - The full source text of the file
  • comment - A ts.CommentRange object containing position and type information

Functions

forEachComment

function forEachComment(
  node: ts.Node,
  callback: ForEachCommentCallback,
  sourceFile?: ts.SourceFile
): void

Iterates over all comments owned by a node or its children, calling the provided callback for each comment found.

Parameters:

  • node - The AST node to analyze for comments
  • callback - Function called for each comment found
  • sourceFile - Optional source file (inferred from node if not provided)

Example - Extracting TODO comments:

import { forEachComment } from "ts-api-utils";
import * as ts from "typescript";

function extractTodoComments(sourceFile: ts.SourceFile): string[] {
  const todos: string[] = [];
  
  forEachComment(sourceFile, (fullText, comment) => {
    const commentText = fullText.slice(comment.pos, comment.end);
    
    if (commentText.toLowerCase().includes('todo')) {
      todos.push(commentText.trim());
    }
  }, sourceFile);
  
  return todos;
}

// Usage
const sourceFile = ts.createSourceFile(
  "example.ts",
  `
  // TODO: Implement error handling
  function process() {
    /* TODO: Add validation */
    return null;
  }
  `,
  ts.ScriptTarget.Latest
);

const todos = extractTodoComments(sourceFile);
console.log(todos); // ["// TODO: Implement error handling", "/* TODO: Add validation */"]

Example - Comment analysis:

import { forEachComment } from "ts-api-utils";

function analyzeComments(node: ts.Node, sourceFile: ts.SourceFile) {
  const commentStats = {
    singleLine: 0,
    multiLine: 0,
    jsdoc: 0
  };
  
  forEachComment(node, (fullText, comment) => {
    const commentText = fullText.slice(comment.pos, comment.end);
    
    if (comment.kind === ts.SyntaxKind.SingleLineCommentTrivia) {
      commentStats.singleLine++;
    } else if (comment.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
      if (commentText.startsWith('/**')) {
        commentStats.jsdoc++;
      } else {
        commentStats.multiLine++;
      }
    }
  }, sourceFile);
  
  return commentStats;
}

Flag Utilities

TypeScript uses various flag systems to track properties of nodes, types, symbols, and other AST elements. These utilities provide safe ways to test these flags.

Functions

isModifierFlagSet

function isModifierFlagSet(
  node: ts.Declaration,
  flag: ts.ModifierFlags
): boolean

Tests if the given declaration node has the specified ModifierFlags set.

Parameters:

  • node - The declaration to test
  • flag - The modifier flag to check for

Returns: true if the flag is set, false otherwise

Example - Checking access modifiers:

import { isModifierFlagSet } from "ts-api-utils";
import * as ts from "typescript";

function analyzeClassMember(member: ts.ClassElement) {
  if (ts.isPropertyDeclaration(member) || ts.isMethodDeclaration(member)) {
    if (isModifierFlagSet(member, ts.ModifierFlags.Private)) {
      console.log(`${member.name?.getText()} is private`);
    }
    
    if (isModifierFlagSet(member, ts.ModifierFlags.Static)) {
      console.log(`${member.name?.getText()} is static`);
    }
    
    if (isModifierFlagSet(member, ts.ModifierFlags.Abstract)) {
      console.log(`${member.name?.getText()} is abstract`);
    }
  }
}

isNodeFlagSet

function isNodeFlagSet(
  node: ts.Node,
  flag: ts.NodeFlags
): boolean

Tests if the given node has the specified NodeFlags set.

Parameters:

  • node - The node to test
  • flag - The node flag to check for

Returns: true if the flag is set, false otherwise

Example - Checking node flags:

import { isNodeFlagSet } from "ts-api-utils";
import * as ts from "typescript";

function analyzeNode(node: ts.Node) {
  if (isNodeFlagSet(node, ts.NodeFlags.Synthesized)) {
    console.log("Node is synthesized (not from original source)");
  }
  
  if (isNodeFlagSet(node, ts.NodeFlags.ThisNodeHasError)) {
    console.log("Node has compilation errors");
  }
  
  if (isNodeFlagSet(node, ts.NodeFlags.HasImplicitReturn)) {
    console.log("Function has implicit return");
  }
}

isObjectFlagSet

function isObjectFlagSet(
  objectType: ts.ObjectType,
  flag: ts.ObjectFlags
): boolean

Tests if the given object type has the specified ObjectFlags set.

Parameters:

  • objectType - The object type to test
  • flag - The object flag to check for

Returns: true if the flag is set, false otherwise

Example - Analyzing object types:

import { isObjectFlagSet } from "ts-api-utils";
import * as ts from "typescript";

function analyzeObjectType(type: ts.Type, typeChecker: ts.TypeChecker) {
  if (type.flags & ts.TypeFlags.Object) {
    const objectType = type as ts.ObjectType;
    
    if (isObjectFlagSet(objectType, ts.ObjectFlags.Interface)) {
      console.log("Type is an interface");
    }
    
    if (isObjectFlagSet(objectType, ts.ObjectFlags.Class)) {
      console.log("Type is a class");
    }
    
    if (isObjectFlagSet(objectType, ts.ObjectFlags.Tuple)) {
      console.log("Type is a tuple");
    }
  }
}

isSymbolFlagSet

function isSymbolFlagSet(
  symbol: ts.Symbol,
  flag: ts.SymbolFlags
): boolean

Tests if the given symbol has the specified SymbolFlags set.

Parameters:

  • symbol - The symbol to test
  • flag - The symbol flag to check for

Returns: true if the flag is set, false otherwise

Example - Symbol analysis:

import { isSymbolFlagSet } from "ts-api-utils";
import * as ts from "typescript";

function analyzeSymbol(symbol: ts.Symbol) {
  if (isSymbolFlagSet(symbol, ts.SymbolFlags.Variable)) {
    console.log("Symbol represents a variable");
  }
  
  if (isSymbolFlagSet(symbol, ts.SymbolFlags.Function)) {
    console.log("Symbol represents a function");
  }
  
  if (isSymbolFlagSet(symbol, ts.SymbolFlags.Class)) {
    console.log("Symbol represents a class");
  }
  
  if (isSymbolFlagSet(symbol, ts.SymbolFlags.Exported)) {
    console.log("Symbol is exported");
  }
}

isTypeFlagSet

function isTypeFlagSet(
  type: ts.Type,
  flag: ts.TypeFlags
): boolean

Tests if the given type has the specified TypeFlags set.

Parameters:

  • type - The type to test
  • flag - The type flag to check for

Returns: true if the flag is set, false otherwise

Example - Type flag checking:

import { isTypeFlagSet } from "ts-api-utils";
import * as ts from "typescript";

function analyzeType(type: ts.Type) {
  if (isTypeFlagSet(type, ts.TypeFlags.String)) {
    console.log("Type is string");
  }
  
  if (isTypeFlagSet(type, ts.TypeFlags.Number)) {
    console.log("Type is number");
  }
  
  if (isTypeFlagSet(type, ts.TypeFlags.Union)) {
    console.log("Type is a union type");
  }
  
  if (isTypeFlagSet(type, ts.TypeFlags.Literal)) {
    console.log("Type is a literal type");
  }
}

Modifier Utilities

Modifiers control various aspects of declarations like visibility, mutability, and behavior.

Functions

includesModifier

function includesModifier(
  modifiers: Iterable<ts.ModifierLike> | undefined,
  ...kinds: ts.ModifierSyntaxKind[]
): boolean

Tests if the given iterable of modifiers includes any modifier of the specified kinds.

Parameters:

  • modifiers - An iterable of modifier-like nodes (can be undefined)
  • ...kinds - One or more modifier syntax kinds to check for

Returns: true if any of the specified modifier kinds are found, false otherwise

Example - Checking for access modifiers:

import { includesModifier } from "ts-api-utils";
import * as ts from "typescript";

function analyzeDeclaration(node: ts.Declaration) {
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  
  if (includesModifier(modifiers, ts.SyntaxKind.PublicKeyword)) {
    console.log("Declaration is public");
  }
  
  if (includesModifier(
    modifiers,
    ts.SyntaxKind.PrivateKeyword,
    ts.SyntaxKind.ProtectedKeyword
  )) {
    console.log("Declaration has restricted access");
  }
  
  if (includesModifier(modifiers, ts.SyntaxKind.StaticKeyword, ts.SyntaxKind.ReadonlyKeyword)) {
    console.log("Declaration is static or readonly");
  }
}

Example - Modifier validation:

import { includesModifier } from "ts-api-utils";
import * as ts from "typescript";

function validateClassMember(member: ts.ClassElement): string[] {
  const errors: string[] = [];
  const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined;
  
  // Check for conflicting access modifiers
  const accessModifiers = [
    ts.SyntaxKind.PublicKeyword,
    ts.SyntaxKind.PrivateKeyword,
    ts.SyntaxKind.ProtectedKeyword
  ];
  
  const hasMultipleAccess = accessModifiers.filter(mod =>
    includesModifier(modifiers, mod)
  ).length > 1;
  
  if (hasMultipleAccess) {
    errors.push("Multiple access modifiers are not allowed");
  }
  
  // Abstract members cannot be private
  if (includesModifier(modifiers, ts.SyntaxKind.AbstractKeyword) &&
      includesModifier(modifiers, ts.SyntaxKind.PrivateKeyword)) {
    errors.push("Abstract members cannot be private");
  }
  
  return errors;
}

Scope Utilities

Scope utilities help determine boundaries where variable scoping rules change, which is crucial for accurate variable analysis.

Functions

isFunctionScopeBoundary

function isFunctionScopeBoundary(node: ts.Node): boolean

Determines whether a node represents a function scope boundary - a point where variables scoping rules change due to function boundaries.

Parameters:

  • node - The node to test for being a function scope boundary

Returns: true if the node is a function scope boundary, false otherwise

Function scope boundaries include:

  • Function declarations
  • Function expressions
  • Arrow functions
  • Method declarations
  • Constructor declarations
  • Getter/setter declarations
  • Module declarations

Example - Variable scope analysis:

import { isFunctionScopeBoundary } from "ts-api-utils";
import * as ts from "typescript";

function analyzeScopes(sourceFile: ts.SourceFile) {
  const scopes: ts.Node[] = [];
  
  function visit(node: ts.Node) {
    if (isFunctionScopeBoundary(node)) {
      scopes.push(node);
      console.log(`Found scope boundary: ${ts.SyntaxKind[node.kind]}`);
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
  return scopes;
}

Example - Scope-aware variable tracking:

import { isFunctionScopeBoundary } from "ts-api-utils";
import * as ts from "typescript";

class ScopeTracker {
  private scopeStack: ts.Node[] = [];
  
  visit(node: ts.Node) {
    if (isFunctionScopeBoundary(node)) {
      this.scopeStack.push(node);
    }
    
    if (ts.isIdentifier(node)) {
      console.log(`Variable '${node.text}' in scope depth: ${this.scopeStack.length}`);
    }
    
    ts.forEachChild(node, (child) => this.visit(child));
    
    if (isFunctionScopeBoundary(node)) {
      this.scopeStack.pop();
    }
  }
}

Syntax Validation Utilities

These utilities help validate and analyze syntax properties and patterns.

Functions

isAssignmentKind

function isAssignmentKind(kind: ts.SyntaxKind): boolean

Tests whether the given syntax kind represents an assignment operation.

Parameters:

  • kind - The syntax kind to test

Returns: true if the kind represents an assignment, false otherwise

Assignment kinds include:

  • EqualsToken (=)
  • PlusEqualsToken (+=)
  • MinusEqualsToken (-=)
  • AsteriskEqualsToken (*=)
  • And other compound assignment operators

Example - Assignment detection:

import { isAssignmentKind } from "ts-api-utils";
import * as ts from "typescript";

function analyzeAssignments(sourceFile: ts.SourceFile) {
  function visit(node: ts.Node) {
    if (ts.isBinaryExpression(node) && isAssignmentKind(node.operatorToken.kind)) {
      console.log(`Assignment: ${node.left.getText()} ${node.operatorToken.getText()} ${node.right.getText()}`);
    }
    
    ts.forEachChild(node, visit);
  }
  
  visit(sourceFile);
}

isNumericPropertyName

function isNumericPropertyName(name: string | ts.__String): boolean

Tests if a string represents a numeric property name (like array indices).

Parameters:

  • name - The property name to test (string or TypeScript internal string)

Returns: true if the name is numeric, false otherwise

Example - Property analysis:

import { isNumericPropertyName } from "ts-api-utils";
import * as ts from "typescript";

function analyzeObjectLiteral(node: ts.ObjectLiteralExpression) {
  for (const property of node.properties) {
    if (ts.isPropertyAssignment(property) && property.name) {
      const nameText = property.name.getText();
      
      if (isNumericPropertyName(nameText)) {
        console.log(`Numeric property: ${nameText}`);
      } else {
        console.log(`Named property: ${nameText}`);
      }
    }
  }
}

// Example usage
const code = `{
  0: "first",
  "1": "second", 
  name: "value",
  "42": "answer"
}`;

isValidPropertyAccess

function isValidPropertyAccess(
  text: string,
  languageVersion?: ts.ScriptTarget
): boolean

Determines whether the given text can be used to access a property with a PropertyAccessExpression while preserving the property's name.

Parameters:

  • text - The property name text to validate
  • languageVersion - Optional TypeScript language version (affects identifier rules)

Returns: true if the text can be used in property access syntax, false otherwise

Example - Property access validation:

import { isValidPropertyAccess } from "ts-api-utils";

// Valid identifiers for property access
console.log(isValidPropertyAccess("name"));        // true
console.log(isValidPropertyAccess("_private"));    // true
console.log(isValidPropertyAccess("$jquery"));     // true

// Invalid - must use bracket notation
console.log(isValidPropertyAccess("123"));         // false (starts with number)
console.log(isValidPropertyAccess("my-prop"));     // false (contains hyphen)
console.log(isValidPropertyAccess("class"));       // false (reserved keyword)

// Usage in code transformation
function createPropertyAccess(object: string, property: string): string {
  if (isValidPropertyAccess(property)) {
    return `${object}.${property}`;
  } else {
    return `${object}[${JSON.stringify(property)}]`;
  }
}

console.log(createPropertyAccess("obj", "name"));     // "obj.name"
console.log(createPropertyAccess("obj", "123"));      // "obj["123"]"
console.log(createPropertyAccess("obj", "my-prop"));  // "obj["my-prop"]"

Token Processing

Token processing utilities allow iteration over and manipulation of syntax tokens, which are the smallest units of syntax.

Types

ForEachTokenCallback

type ForEachTokenCallback = (token: ts.Node) => void

Callback type used with forEachToken to process each token found in the AST.

Parameters:

  • token - The token node being processed

Functions

forEachToken

function forEachToken(
  node: ts.Node,
  callback: ForEachTokenCallback,
  sourceFile?: ts.SourceFile
): void

Iterates over all tokens of a node, calling the provided callback for each token found.

Parameters:

  • node - The AST node to analyze for tokens
  • callback - Function called for each token found
  • sourceFile - Optional source file (inferred from node if not provided)

Example - Token counting:

import { forEachToken } from "ts-api-utils";
import * as ts from "typescript";

function countTokens(node: ts.Node, sourceFile: ts.SourceFile): number {
  let count = 0;
  
  forEachToken(node, (token) => {
    count++;
  }, sourceFile);
  
  return count;
}

// Usage
const sourceFile = ts.createSourceFile(
  "example.ts",
  "function hello() { return 'world'; }",
  ts.ScriptTarget.Latest
);

const tokenCount = countTokens(sourceFile, sourceFile);
console.log(`Total tokens: ${tokenCount}`);

Example - Token analysis:

import { forEachToken } from "ts-api-utils";
import * as ts from "typescript";

function analyzeTokens(node: ts.Node, sourceFile: ts.SourceFile) {
  const tokenStats = new Map<ts.SyntaxKind, number>();
  
  forEachToken(node, (token) => {
    const kind = token.kind;
    tokenStats.set(kind, (tokenStats.get(kind) || 0) + 1);
  }, sourceFile);
  
  // Report most common tokens
  const sorted = Array.from(tokenStats.entries())
    .sort(([,a], [,b]) => b - a)
    .slice(0, 5);
    
  console.log("Most common tokens:");
  sorted.forEach(([kind, count]) => {
    console.log(`${ts.SyntaxKind[kind]}: ${count}`);
  });
}

Example - Syntax highlighting preparation:

import { forEachToken } from "ts-api-utils";
import * as ts from "typescript";

interface TokenInfo {
  kind: ts.SyntaxKind;
  text: string;
  start: number;
  end: number;
  category: string;
}

function prepareTokensForHighlighting(sourceFile: ts.SourceFile): TokenInfo[] {
  const tokens: TokenInfo[] = [];
  
  forEachToken(sourceFile, (token) => {
    const category = categorizeToken(token.kind);
    
    tokens.push({
      kind: token.kind,
      text: token.getText(sourceFile),
      start: token.getStart(sourceFile),
      end: token.getEnd(),
      category
    });
  }, sourceFile);
  
  return tokens;
}

function categorizeToken(kind: ts.SyntaxKind): string {
  if (kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword) {
    return "keyword";
  }
  if (kind === ts.SyntaxKind.StringLiteral) {
    return "string";
  }
  if (kind === ts.SyntaxKind.NumericLiteral) {
    return "number";
  }
  if (kind === ts.SyntaxKind.Identifier) {
    return "identifier";
  }
  return "punctuation";
}

Practical Examples

Complete Syntax Analyzer

import {
  forEachComment,
  forEachToken,
  isModifierFlagSet,
  includesModifier,
  isFunctionScopeBoundary,
  isAssignmentKind,
  isValidPropertyAccess
} from "ts-api-utils";
import * as ts from "typescript";

class ComprehensiveSyntaxAnalyzer {
  private sourceFile: ts.SourceFile;
  
  constructor(sourceCode: string, fileName: string = "analysis.ts") {
    this.sourceFile = ts.createSourceFile(
      fileName,
      sourceCode,
      ts.ScriptTarget.Latest,
      true
    );
  }
  
  analyze() {
    const results = {
      comments: this.analyzeComments(),
      tokens: this.analyzeTokens(),
      scopes: this.analyzeScopes(),
      assignments: this.analyzeAssignments(),
      modifiers: this.analyzeModifiers()
    };
    
    return results;
  }
  
  private analyzeComments() {
    const comments: string[] = [];
    
    forEachComment(this.sourceFile, (fullText, comment) => {
      const text = fullText.slice(comment.pos, comment.end).trim();
      comments.push(text);
    }, this.sourceFile);
    
    return {
      count: comments.length,
      comments: comments
    };
  }
  
  private analyzeTokens() {
    const tokenCounts = new Map<string, number>();
    
    forEachToken(this.sourceFile, (token) => {
      const kindName = ts.SyntaxKind[token.kind];
      tokenCounts.set(kindName, (tokenCounts.get(kindName) || 0) + 1);
    }, this.sourceFile);
    
    return Object.fromEntries(tokenCounts);
  }
  
  private analyzeScopes() {
    const scopes: string[] = [];
    
    const visit = (node: ts.Node) => {
      if (isFunctionScopeBoundary(node)) {
        scopes.push(ts.SyntaxKind[node.kind]);
      }
      ts.forEachChild(node, visit);
    };
    
    visit(this.sourceFile);
    return scopes;
  }
  
  private analyzeAssignments() {
    const assignments: string[] = [];
    
    const visit = (node: ts.Node) => {
      if (ts.isBinaryExpression(node) && isAssignmentKind(node.operatorToken.kind)) {
        assignments.push(node.getText());
      }
      ts.forEachChild(node, visit);
    };
    
    visit(this.sourceFile);
    return assignments;
  }
  
  private analyzeModifiers() {
    const modifierUsage = new Map<string, number>();
    
    const visit = (node: ts.Node) => {
      if (ts.canHaveModifiers(node)) {
        const modifiers = ts.getModifiers(node);
        if (modifiers) {
          for (const modifier of modifiers) {
            const modifierName = ts.SyntaxKind[modifier.kind];
            modifierUsage.set(modifierName, (modifierUsage.get(modifierName) || 0) + 1);
          }
        }
      }
      ts.forEachChild(node, visit);
    };
    
    visit(this.sourceFile);
    return Object.fromEntries(modifierUsage);
  }
}

// Usage
const code = `
// Main application class
class Application {
  private static instance: Application;
  
  /** 
   * Creates new instance
   */
  constructor() {
    this.count = 0;
  }
  
  public start(): void {
    console.log("Starting...");
  }
}
`;

const analyzer = new ComprehensiveSyntaxAnalyzer(code);
const results = analyzer.analyze();
console.log(results);

Best Practices

Safe Flag Checking

// ✅ Good: Use utility functions for flag checking
if (isModifierFlagSet(node, ts.ModifierFlags.Static)) {
  // Handle static member
}

// ❌ Avoid: Direct flag manipulation (error-prone)
if (node.modifierFlagsCache & ts.ModifierFlags.Static) {
  // This accesses internal implementation details
}

Comprehensive Token Analysis

// ✅ Good: Use forEachToken for complete analysis
function analyzeAllTokens(node: ts.Node, sourceFile: ts.SourceFile) {
  forEachToken(node, (token) => {
    // Process each token individually
    processToken(token);
  }, sourceFile);
}

// ❌ Avoid: Manual token traversal (misses edge cases)
function manualTokenWalk(node: ts.Node) {
  // This approach can miss tokens in complex expressions
}

Proper Scope Tracking

// ✅ Good: Use isFunctionScopeBoundary for accurate scope detection
class ScopeAnalyzer {
  private scopeDepth = 0;
  
  visit(node: ts.Node) {
    if (isFunctionScopeBoundary(node)) {
      this.scopeDepth++;
    }
    
    // Analyze node at current scope depth
    
    ts.forEachChild(node, (child) => this.visit(child));
    
    if (isFunctionScopeBoundary(node)) {
      this.scopeDepth--;
    }
  }
}

The Syntax Utilities provide essential building blocks for sophisticated TypeScript code analysis, enabling developers to build tools that understand the nuanced structure and semantics of TypeScript syntax at both high and low levels of detail.

Install with Tessl CLI

npx tessl i tessl/npm-ts-api-utils

docs

compiler-options.md

index.md

node-analysis.md

syntax-utilities.md

type-system.md

usage-analysis.md

tile.json