How do you visualize a TypeScript dependency graph? A developer tutorial demonstrates building a command-line utility called ts-graph that uses the TypeScript Compiler API to scan a TypeScript project and print its file dependency graph. The tool creates a Program object, filters Program.getSourceFiles() to exclude declaration files, and traverses each SourceFile's Abstract Syntax Tree to collect ImportDeclaration nodes, producing a tree output rooted at App.ts. The approach lets developers detect circular dependencies and analyze architecture across projects with hundreds or thousands of TypeScript files. 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: js 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: js 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: js 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