How do you visualize a TypeScript dependency graph?
Rosario De ChiaraI'm a blockchain technology lead. My passions are distributed systems, efficient algorithms, and retrocomputing. I have a PhD (Dottorato di Ricerca) in Computer Science and worked as a researcher at university. I’m Italian, which means I’m pretty opinionated about food.
See how LogRocket's Galileo AI surfaces the most severe issues for you
No signup required
Check it out
Most frontend applications eventually grow into hundreds or thousands of TypeScript files. Before you can analyze architecture, detect circular dependencies, or build developer tools, you need to answer a very simple question:
Which files depend on which other files?
The TypeScript Compiler API already contains this information. Every source file is parsed into an Abstract Syntax Tree (AST), where import statements become nodes that can be inspected programmatically.
As usual, the fully functional code is available in a GitHub repository.
In this article, we’ll build a tiny command-line utility called ts-graph that scans a TypeScript project and prints its dependency graph.
The final output will look like this:
App.ts
├── Home.ts
│ ├── homeMessages.ts
│ │ ├── textCase.ts
│ │ └── clock.ts
│ └── userSession.ts
│ └── envFlags.ts
├── Button.ts
│ └── buttonStyles.ts
│ └── designTokens.ts
├── utils.ts
│ └── textCase.ts
└── theme.ts
├── designTokens.ts
└── envFlags.ts
Along the way, we’ll introduce four fundamental Compiler API concepts:
Program — Represents the entire TypeScript project being analyzed
SourceFile — Represents a single TypeScript file and its AST
ImportDeclaration — An AST node representing an import statement
The idea of the project is to create a Program object starting from the entry file of the project you want to analyze (in the repository, we will use the sample directory); then we will run through the SourceFile objects in the target directory; these will be the nodes of our tree of dependencies. To understand which files are imported, for each SourceFile object we will analyze its AST to check the ImportDeclaration nodes and, eventually, build up the dependency graph.
How to create a TypeScript program object
The Compiler API always starts by creating a Program. If the AST is the parsed code, the Program is the thing that owns all parsed code.
In our tiny CLI, this is the core setup:
const program = ts.createProgram(
[],
{}
);
Once you have the program object, you can access the Program.getSourceFiles() method, which returns every SourceFile loaded into the TypeScript Program. That means it includes files from the target sample directory, the chosen entry file and its imported dependencies, plus TypeScript library declaration files and any extra files brought in by compiler resolution. In the following lines, we will filter these entries so that only files belonging to the folder containing the project you want to analyze are kept for graph building, while external and standard library files are skipped.
Even if your end goal is “just parse imports”, you still begin here. You get a single coherent model of the project instead of trying to parse files ad hoc, one by one.
How to traverse source files in a TypeScript project
A SourceFile represents a single parsed TypeScript file and serves as the root of its Abstract Syntax Tree (AST). In our CLI, we retrieve these files from the Program and ignore declaration files, since we are interested only in the application source code:
const sourceFiles = program
.getSourceFiles()
.filter(sf => !sf.isDeclarationFile);
Each SourceFile contains an AST whose nodes form a tree representing the structure of the code. We can traverse this tree recursively and look for specific nodes – in our case, ImportDeclaration nodes that tell us which other files a source file depends on.
This simple pattern – walk the AST and collect useful information- is at the heart of many Compiler API tools. Linters, codemods, static analyzers, and architecture tools all use variations of the same approach to inspect source code and extract meaningful signals.
How to extract import declarations from the TypeScript AST
To traverse an AST, the TypeScript Compiler API uses a simple visitor pattern: we define a function that receives a node, inspects it to see whether it contains information we’re interested in, and then continues the traversal through its children.
For our dependency graph, we’re interested in ImportDeclaration nodes because they represent import statements. The visitor can therefore remain remarkably small:
function visit(node: ts.Node) {
if (ts.isImportDeclaration(node)) {
// extract dependency
}
ts.forEachChild(node, visit);
}
The ts.forEachChild() call is what makes the function recursive. Every AST node can have children, so after inspecting the current node, we simply ask TypeScript to visit each of those children using the same function. As a result, the visitor walks the entire AST without us having to know the structure of every possible TypeScript construct.
For our particular problem, ts.isImportDeclaration() acts as the filter: whenever the traversal encounters an import statement, we have found a dependency we can add to our graph.
This small piece of code captures the essence of working with the Compiler API: walk the tree, identify the nodes you care about, and extract information from them.
How to build and structure the dependency map
We now have all the pieces we need to build the dependency graph. The process is straightforward: for each SourceFile in the Program, we traverse its AST and look for ImportDeclaration nodes. Each import represents an edge from the current file to another file, which we store in the graph.
At its simplest, the core of the algorithm looks like this:
for (const sourceFile of program.getSourceFiles()) {
const dependencies: string[] = [];
visitNode(sourceFile, dependencies);
graph.set(sourceFileName, dependencies);
}
The visitor does the actual extraction. Whenever it encounters an ImportDeclaration, we take its module specifier and add it to the list of dependencies:
function visitNode(node: ts.Node, dependencies: string[]) {
if (ts.isImportDeclaration(node)) {
dependencies.push(node.moduleSpecifier.text);
}
ts.forEachChild(node, child => visitNode(child, dependencies));
}
The resulting data structure is deliberately simple: a map where each file is associated with the files it imports.
Map<string, string[]>
For example, the imports in App.ts can be represented as:
App.ts → [Home.ts, Button.ts, utils.ts]
There is no sophisticated graph algorithm or special data structure involved here. The interesting part is not how the dependencies are stored, but how we extract them from the AST. The Compiler API gives us the structure of the source code; our visitor turns that structure into information we can use.
This is the fundamental pattern behind the tool: the Program gives us access to the project, each SourceFile provides an AST, the visitor extracts ImportDeclaration nodes, and those imports become edges in our dependency graph. Once we have this graph, we can start doing something useful with it.
Conclusion
This code is small, but it is not a toy in the dismissive sense. It is the first real building block for serious tooling.
The dependency graph is the foundation for many developer tools: architecture visualizers, circular dependency detectors, unused module analysis, impact analysis, bundlers, and IDE features all begin by understanding how source files relate to one another.
The code in the Repository is designed to implement a proper shell command: it takes an argument that is the directory containing the project you want to analyze, so even if simple, it is a proper boilerplate for more sophisticated functionalities.
From here, we could move toward enriching the graph with additional metadata, such as exported symbols, allowing us to move from file-level dependencies to symbol-level relationships. That small step opens the door to building much more sophisticated tooling on top of the TypeScript Compiler API.
Stop generating AI slop with Claude Code. Discover 5 actionable developer tips to manage context windows, enforce rules with hooks, and improve code quality.
Choosing between skills and MCP tools comes down to auditability versus flexibility. By building the exact same capability twice, this guide reveals when your agent needs a deterministic tool and when it needs an interpretive skill.
Learn how to replace React state, Context, and event handlers with native HTML and CSS features for dark mode, modals, accordions, carousels, and more.
A real-app benchmark of cnfast’s drop-in cn() replacement: isolated speed tests look great, but does any of it survive contact with an actual React render?
Would you be interested in joining LogRocket's developer community?
YeaNo Thanks
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.