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 theSyntaxKindenum 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'sts.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()— theSourceFilethis node lives in.getAncestors(),getFirstAncestorByKind(kind).
Type checking / kind narrowing
asKind(SyntaxKind.X)— returns the node cast to that kind, orundefinedif 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 theNodenamespace/export, used to narrow a genericNodeto 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 aTypeobject (requires a type checker, i.e. an actualProject, 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.). HasgetStatements(). - ModuleDeclaration —
namespace Foo { }/module "foo" { }. - ModuleBlock — the body of a module/namespace.
B. Declaration nodes
These represent named things being declared:
- ClassDeclaration / ClassExpression —
getName(),getExtends(),getImplements(),getConstructors(),getProperties(),getMethods(),getMethod(name),addMethod(structure),addProperty(structure),isAbstract(),getBaseClass(). - InterfaceDeclaration —
getProperties(),getMethods(),getExtends(),addProperty(structure),addMethod(structure). - FunctionDeclaration / FunctionExpression / ArrowFunction —
getParameters(),getReturnType(),getBody(),isAsync(),isGenerator(),addParameter(structure). - EnumDeclaration and EnumMember —
getMembers(),addMember(structure). - TypeAliasDeclaration —
getTypeNode(),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, NamespaceImport —
getModuleSpecifierValue(),getNamedImports(),getDefaultImport(),addNamedImport(name). - ExportDeclaration, ExportSpecifier, ExportAssignment — analogous export-side methods.
- ImportEqualsDeclaration —
import x = require(...)style.
C. Class member nodes
- PropertyDeclaration —
getType(),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.mdfile 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
.compilerNodeto reach the rawts.Nodefrom the underlying TypeScript compiler API.
Comments
Post a Comment