AST (Abstract Syntax Tree)

ts-morph: A Full Guide to Available Nodes and How to Use Them

ts-morph is a TypeScript library that wraps the raw TypeScript Compiler API in friendlier, chainable classes. Instead of manually walking ts.Node objects and calling low-level compiler functions, you work with typed wrapper classes — ClassDeclaration, FunctionDeclaration, CallExpression, and roughly 220+ others — each exposing methods for reading, navigating, and rewriting that piece of source.

Every wrapper ultimately extends a base Node class, so understanding that base class first makes everything else click into place.

1. The Node base class

Every specific node type (ClassDeclaration, Identifier, IfStatement, etc.) inherits from Node. This gives all of them a shared set of capabilities:

Identity & text

  • getKind() — returns the SyntaxKind enum value (e.g. SyntaxKind.ClassDeclaration).
  • getKindName() — the human-readable string version of the kind.
  • getText() — the exact source text of the node.
  • getFullText() — source text including leading trivia (whitespace/comments).
  • print() — pretty-printed text of the node.

Position

  • getStart(), getEnd(), getPos() — character offsets in the file.
  • getStartLineNumber(), getEndLineNumber().

Tree navigation

  • getParent(), getParentOrThrow(), getParentWhile(...).
  • getChildren() — all child nodes, including punctuation tokens (braces, semicolons).
  • forEachChild(cb) — iterates only the "meaningful" child nodes (mirrors the compiler API's ts.forEachChild).
  • forEachDescendant(cb) — recursively visits every descendant; supports a traversal control object (traversal.skip(), traversal.up(), traversal.stop()) to prune or halt the walk.
  • getFirstChild(), getLastChild(), getFirstChildByKind(kind), getFirstDescendantByKind(kind), getChildAtIndex(i).
  • getSourceFile() — the SourceFile this node lives in.
  • getAncestors(), getFirstAncestorByKind(kind).

Type checking / kind narrowing

  • asKind(SyntaxKind.X) — returns the node cast to that kind, or undefined if it doesn't match.
  • asKindOrThrow(SyntaxKind.X) — same, but throws if it doesn't match.
  • Node.isClassDeclaration(node), Node.isCallExpression(node), etc. — a large family of static type-guard functions on the Node namespace/export, used to narrow a generic Node to its specific subtype in TypeScript-safe code.

Manipulation

  • replaceWithText(text) — replaces the node's text.
  • remove() — deletes the node (available on nodes that support removal, e.g. statements, class members).
  • getType() — returns a Type object (requires a type checker, i.e. an actual Project, not just a standalone wrapped node).
import { Project, Node } from "ts-morph";

const project = new Project();
const sourceFile = project.createSourceFile("test.ts", `
  class Foo {
    bar() { return 1; }
  }
`);

sourceFile.forEachDescendant((node) => {
  if (Node.isClassDeclaration(node)) {
    console.log(node.getName()); // "Foo"
  }
});

2. Categories of wrapped nodes

ts-morph wraps roughly 224 distinct TypeScript AST node types (plus a handful of not-yet-wrapped ones that still work as generic Nodes). They fall into natural groups:

A. Structural / container nodes

  • SourceFile — the root node for a file. Has methods like getClasses(), getInterfaces(), getFunctions(), getVariableStatements(), getImportDeclarations(), getExportDeclarations(), addClass(structure), insertStatements(index, text), save(), emit().
  • Block — a { } statement body (function bodies, if-blocks, etc.). Has getStatements().
  • ModuleDeclarationnamespace Foo { } / module "foo" { }.
  • ModuleBlock — the body of a module/namespace.

B. Declaration nodes

These represent named things being declared:

  • ClassDeclaration / ClassExpressiongetName(), getExtends(), getImplements(), getConstructors(), getProperties(), getMethods(), getMethod(name), addMethod(structure), addProperty(structure), isAbstract(), getBaseClass().
  • InterfaceDeclarationgetProperties(), getMethods(), getExtends(), addProperty(structure), addMethod(structure).
  • FunctionDeclaration / FunctionExpression / ArrowFunctiongetParameters(), getReturnType(), getBody(), isAsync(), isGenerator(), addParameter(structure).
  • EnumDeclaration and EnumMembergetMembers(), addMember(structure).
  • TypeAliasDeclarationgetTypeNode(), setType(text).
  • VariableStatement, VariableDeclarationList, VariableDeclaration — a statement like const x = 1, y = 2; decomposes into a statement wrapping a declaration list wrapping individual declarations. getDeclarations(), getDeclarationKind() (const/let/var), getInitializer().
  • ImportDeclaration, ImportClause, ImportSpecifier, NamespaceImportgetModuleSpecifierValue(), getNamedImports(), getDefaultImport(), addNamedImport(name).
  • ExportDeclaration, ExportSpecifier, ExportAssignment — analogous export-side methods.
  • ImportEqualsDeclarationimport x = require(...) style.

C. Class member nodes

  • PropertyDeclarationgetType(), setType(), getInitializer(), hasModifier(SyntaxKind.ReadonlyKeyword).
  • MethodDeclaration — same surface as a function plus getScope() (public/private/protected), isStatic().
  • GetAccessorDeclaration / SetAccessorDeclaration — getter/setter pairs.
  • ConstructorDeclaration — constructor-specific handling, including constructor overloads.
  • Decorator@Component(...)-style decorators; getName(), getArguments().

D. Interface / type-literal member nodes

PropertySignature, MethodSignature, CallSignatureDeclaration, ConstructSignatureDeclaration, IndexSignatureDeclaration — the interface/type-literal equivalents of class members, minus implementations.

E. Statement nodes

Control flow and other statements each get their own class: IfStatement, ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoStatement, SwitchStatement (with CaseBlock, CaseClause, DefaultClause), TryStatement (with CatchClause), ReturnStatement, ThrowStatement, BreakStatement, ContinueStatement, LabeledStatement, ExpressionStatement, EmptyStatement, DebuggerStatement. Each exposes accessors for its specific parts (e.g. IfStatement.getThenStatement() / getElseStatement()).

F. Expression nodes

The largest group: CallExpression, NewExpression, BinaryExpression, ConditionalExpression (ternary), PropertyAccessExpression (a.b), ElementAccessExpression (a[b]), ArrayLiteralExpression, ObjectLiteralExpression (with PropertyAssignment, ShorthandPropertyAssignment, SpreadAssignment), TemplateExpression (with TemplateHead/TemplateMiddle/TemplateTail/TemplateSpan), TaggedTemplateExpression, AsExpression, SatisfiesExpression, TypeAssertion (<Type>expr), NonNullExpression (expr!), AwaitExpression, YieldExpression, SpreadElement, ParenthesizedExpression, PrefixUnaryExpression/PostfixUnaryExpression (++x, x++), DeleteExpression, TypeOfExpression, VoidExpression.

G. Literals & identifiers

StringLiteral, NumericLiteral, BigIntLiteral, RegularExpressionLiteral, NoSubstitutionTemplateLiteral, TrueLiteral/FalseLiteral, NullLiteral, Identifier, PrivateIdentifier (#field), ComputedPropertyName, QualifiedName (A.B in a type position).

H. Type nodes

Everything that can appear in a type position gets its own class: TypeReferenceNode (Foo<T>), UnionTypeNode, IntersectionTypeNode, TupleTypeNode, ArrayTypeNode, FunctionTypeNode, ConstructorTypeNode, TypeLiteralNode (inline { ... } type), MappedTypeNode, ConditionalTypeNode (T extends U ? X : Y), IndexedAccessTypeNode (T[K]), TypeQueryNode (typeof x), TypeOperatorNode (keyof/readonly), LiteralTypeNode, ParenthesizedTypeNode, RestTypeNode, InferTypeNode, TemplateLiteralTypeNode, ImportTypeNode, ThisTypeNode, TypeParameterDeclaration, TypePredicateNode (x is Foo).

I. JSDoc nodes

JSDoc, JSDocTag and a long list of specific tags — JSDocParameterTag, JSDocReturnTag, JSDocTypeTag, JSDocDeprecatedTag, JSDocTemplateTag, JSDocSeeTag, JSDocAugmentsTag, and more — plus JSDoc-flavored type nodes like JSDocNullableType, JSDocOptionalType, JSDocFunctionType.

J. JSX nodes

JsxElement, JsxSelfClosingElement, JsxOpeningElement/JsxClosingElement, JsxFragment, JsxAttribute, JsxSpreadAttribute, JsxExpression, JsxText, JsxNamespacedName.

K. Bindings & destructuring

ArrayBindingPattern, ObjectBindingPattern, BindingElement, ArrayDestructuringAssignment, ObjectDestructuringAssignment.

Not every one of these classes will have deep helper methods — some (mostly rarer or newer syntax nodes) are wrapped only enough to be identified and traversed, without rich accessors. The project's wrapped-nodes.md report in the ts-morph GitHub repo tracks exactly which properties of each node are exposed with helper methods versus left as raw children.

3. How you typically get hold of nodes

You start from a Project, which manages one or more SourceFiles in memory:

import { Project, SyntaxKind, Node } from "ts-morph";

const project = new Project({
  tsConfigFilePath: "tsconfig.json", // optional
});

const sourceFile = project.addSourceFileAtPath("src/example.ts");
// or: project.createSourceFile("path.ts", "const x = 1;");

From there, three main strategies find nodes:

a) Named getters — the most common approach, using methods generated for common declaration types:

const classDec = sourceFile.getClassOrThrow("MyClass");
const method = classDec.getMethodOrThrow("doSomething");
const importDecs = sourceFile.getImportDeclarations();

b) getFirstDescendantByKind / getDescendantsOfKind — search by SyntaxKind when there's no named getter:

const arrowFns = sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction);

c) forEachDescendant with type guards — for full control over traversal, including skipping subtrees:

sourceFile.forEachDescendant((node, traversal) => {
  if (Node.isClassDeclaration(node) && node.getName() === "Foo") {
    traversal.stop();
  }
});

4. Reading vs. manipulating

Most node classes expose a matched pair of read/write methods:

Read Write
getName() rename(newName)
getType() / getTypeNode() setType(text)
getInitializer() setInitializer(text)
getModifiers() / hasModifier(kind) addModifier(kind) / toggleModifier(kind)
getParameters() addParameter(structure) / insertParameter(index, structure)
getStructure() set(structure)

The getStructure() / set(structure) pair is especially powerful: getStructure() serializes a node into a plain object describing it (a "Structure", e.g. ClassDeclarationStructure), and set(structure) (or the corresponding add*/insert* methods on a parent) lets you build or rewrite nodes declaratively instead of assembling text by hand:

classDec.addProperty({
  name: "count",
  type: "number",
  initializer: "0",
});

For direct text control, replaceWithText(text) swaps a node's source text verbatim, and remove() deletes it (available on statements, class members, parameters, and similar removable nodes).

After making changes in memory, call sourceFile.save() (or project.save()) to write them to disk.

5. A worked example: renaming and refactoring

import { Project, Node } from "ts-morph";

const project = new Project();
const sourceFile = project.addSourceFileAtPath("src/app.ts");

// Find every class that extends "BaseController" and rename its constructor's
// first parameter to "ctx".
sourceFile.forEachDescendant((node, traversal) => {
  if (Node.isClassDeclaration(node)) {
    const extendsExpr = node.getExtends();
    if (!extendsExpr || extendsExpr.getText() !== "BaseController") {
      traversal.skip(); // don't bother descending into this class
      return;
    }
  } else if (Node.isConstructorDeclaration(node)) {
    const firstParam = node.getParameters()[0];
    if (firstParam) firstParam.rename("ctx");
  }
});

sourceFile.saveSync();

This mirrors the two navigation styles covered above: Node.isXxx guards to narrow types, and forEachDescendant's traversal object to prune the search once a branch is known to be irrelevant.

6. Where to go deeper

  • The official docs (ts-morph.com) cover setup, navigation, and manipulation in more depth.
  • The wrapped-nodes.md file in the dsherret/ts-morph GitHub repo lists every wrapped node class and exactly which of its properties have helper methods versus being left as raw Nodes — the most authoritative "what's available" reference, since IDE autocomplete on a Node subtype instance is otherwise the fastest way to discover what a given class exposes.
  • For anything not wrapped with rich helpers, you can always fall back to .compilerNode to reach the raw ts.Node from the underlying TypeScript compiler API.

Comments

Popular posts from this blog

JAudioTagger - ID3 tagger library

Flutter - Create Image Container with Round Corners and Splash Effect

Maven - Create Executable Jar