Utility functions for working with TypeScript's API, providing comprehensive tools for analyzing and manipulating TypeScript AST nodes, types, and compiler APIs.
79
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.
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:
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 are an important part of code documentation and can contain significant information for analysis tools.
type ForEachCommentCallback = (fullText: string, comment: ts.CommentRange) => voidCallback type used with forEachComment to process each comment found in the source code.
Parameters:
fullText - The full source text of the filecomment - A ts.CommentRange object containing position and type informationfunction forEachComment(
node: ts.Node,
callback: ForEachCommentCallback,
sourceFile?: ts.SourceFile
): voidIterates 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 commentscallback - Function called for each comment foundsourceFile - 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;
}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.
function isModifierFlagSet(
node: ts.Declaration,
flag: ts.ModifierFlags
): booleanTests if the given declaration node has the specified ModifierFlags set.
Parameters:
node - The declaration to testflag - The modifier flag to check forReturns: 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`);
}
}
}function isNodeFlagSet(
node: ts.Node,
flag: ts.NodeFlags
): booleanTests if the given node has the specified NodeFlags set.
Parameters:
node - The node to testflag - The node flag to check forReturns: 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");
}
}function isObjectFlagSet(
objectType: ts.ObjectType,
flag: ts.ObjectFlags
): booleanTests if the given object type has the specified ObjectFlags set.
Parameters:
objectType - The object type to testflag - The object flag to check forReturns: 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");
}
}
}function isSymbolFlagSet(
symbol: ts.Symbol,
flag: ts.SymbolFlags
): booleanTests if the given symbol has the specified SymbolFlags set.
Parameters:
symbol - The symbol to testflag - The symbol flag to check forReturns: 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");
}
}function isTypeFlagSet(
type: ts.Type,
flag: ts.TypeFlags
): booleanTests if the given type has the specified TypeFlags set.
Parameters:
type - The type to testflag - The type flag to check forReturns: 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");
}
}Modifiers control various aspects of declarations like visibility, mutability, and behavior.
function includesModifier(
modifiers: Iterable<ts.ModifierLike> | undefined,
...kinds: ts.ModifierSyntaxKind[]
): booleanTests 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 forReturns: 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 help determine boundaries where variable scoping rules change, which is crucial for accurate variable analysis.
function isFunctionScopeBoundary(node: ts.Node): booleanDetermines 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 boundaryReturns: true if the node is a function scope boundary, false otherwise
Function scope boundaries include:
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();
}
}
}These utilities help validate and analyze syntax properties and patterns.
function isAssignmentKind(kind: ts.SyntaxKind): booleanTests whether the given syntax kind represents an assignment operation.
Parameters:
kind - The syntax kind to testReturns: true if the kind represents an assignment, false otherwise
Assignment kinds include:
EqualsToken (=)PlusEqualsToken (+=)MinusEqualsToken (-=)AsteriskEqualsToken (*=)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);
}function isNumericPropertyName(name: string | ts.__String): booleanTests 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"
}`;function isValidPropertyAccess(
text: string,
languageVersion?: ts.ScriptTarget
): booleanDetermines 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 validatelanguageVersion - 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 utilities allow iteration over and manipulation of syntax tokens, which are the smallest units of syntax.
type ForEachTokenCallback = (token: ts.Node) => voidCallback type used with forEachToken to process each token found in the AST.
Parameters:
token - The token node being processedfunction forEachToken(
node: ts.Node,
callback: ForEachTokenCallback,
sourceFile?: ts.SourceFile
): voidIterates over all tokens of a node, calling the provided callback for each token found.
Parameters:
node - The AST node to analyze for tokenscallback - Function called for each token foundsourceFile - 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";
}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);// ✅ 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
}// ✅ 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
}// ✅ 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-utilsevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10