Easier TypeScript tooling with TSQuery

Query a TypeScript AST with CSS selectors

Craig Spence
6 min readMay 29, 2018

TLDR; 🔥🔥🔥

Need to use the TypeScript APIs for traversing AST nodes? TSQuery might make that a little bit easier. You can use CSS-ish selectors to query the Abstract Syntax Tree, just like you would query the DOM tree:

An example of using tsquery to find AST nodes using CSS-like selectors!

If you’re not sure what an AST is, or just want to a bit of a reminder, check out this talk I gave at JS Conf AU 2018 for a refresher, or just read on further!

❤️ Love it already? Check out the code on Github.

🦄 Try it out! Have a play in the playground made by Uri Shaked

⁉️ Want to know more? Read on…

What is TSQuery?

TSQuery is a rewrite of ESQuery for TypeScript. ESQuery is a neat little library that allows you to use CSS-like selectors to query a JavaScript AST. That turns out to be a really powerful and flexible way to get information about a piece of JavaScript code! Over the past few months I’ve been writing more TS tooling than JS, and I’ve really missed the power of ESQuery — so I decided to port it over.

A whole range of selectors are supported:

  • AST node types: ClassDeclaration, Identifier, VariableDeclaration, etc.
  • Attributes: [name], [left.value=4], [name.name=/^I.*/] , [statements.length<3]
  • :first-child, :last-child, nth-child(6)
  • node descendant, node > child, node ~ sibling, node + adjacent
  • and more!

If we look back at our example of a selector from above, we can break it down and explain what’s going on:

An example of a TSQuery selector.

Here we start with a query for a specific Node type, a ClassDeclaration. We want to find one where the name property (which is an Identifier node) has a name with the value "MyClass". We then want to find a direct descendent of that which is a Node of type Constructor. We can run this query over the code from above and get an array full on all the matching nodes. In this case, there would only be one result: the node for the constructor!

🌈 Neat!

So, why do we need TSQuery?

I’ve been working with TypeScript a lot lately, and I really love it ❤️! If you went back and told that to the 2014 version of me, I probably wouldn’t believe you, but it’s true!

In my ~4 years of using TypeScript in a fairly large AngularJS/Angular project, I’ve found that adding types to JavaScript makes it easier to build stuff good™, and to maintain that stuff as a codebase grows and evolves. Having types makes it easier to reason about my code, makes refactoring safer, and generally gives me more confidence in the software I create and ship 🚢.

Those reasons alone would be enough to make me really love TypeScript. But the real killer feature is the incredible ecosystem of tooling that has been developed by the TypeScript team and community, including:

All these things are built on top of the TypeScript language, and they combine to make a very powerful ecosystem!

But it’s not all roses 🌹🌹🌹…

These tools are all great to use, but writing them can be quite a painful experience. There are lots of barriers to getting started with creating your own tools. To me, the biggest obstacle is getting your head around the idea of an Abstract Syntax Tree (AST), and how you can interrogate and manipulate one.

An AST is a data structure that represents the structure of code in a programming language, without any actual syntax. It describes the ideas that make up a piece of code, without talking about the specific keywords, or specific syntactical tokens.

You can read more about ASTs in this excellent article by Gabriele Petronella

A tree (but not abstract, nor related to syntax).

An example of an AST could look something like this:

An example of an AST for of some TypeScript code.

The “abstract” nature of the data structure is important, as it means that an AST doesn’t necessarily correlate to any particular programming language. It also means that you no longer need to use regular expressions or string manipulation to decipher or modify your source code! Instead, you can refer to parts of the code using the underlying concepts, whether it be the idea of a class, or the fact that a class has a name, or a constructor.

The above AST is a description of the following piece of TypeScript code:

A bit of TypeScript, showing a class called “MyClass”, and its constructor

Let’s say we want to know something about the constructor of MyClass: Does it take any arguments? Does it actually do anything? We could look at the code directly and find out the answers, but we could also find out by looking at the AST.

TypeScript gives us an easy way to create the AST of a block of code, with the createSourceFile() function. It can be used like this:

TypeScript code that uses the createSourceFile() function to create an AST.

Et voilà, we now have a SourceFile object. If we print it out, we can see the same tree structure of the AST as before:

The AST structure of the code from above, as generated by TypeScript. Note that the kind is now a number — TypeScript uses the SyntaxKind enum to represent the node type.

Looking at this we can start to see the parent — child relationships between the nodes. We have a SourceFile (the root node of the AST), which has a series of statements. The first statement is a ClassDeclaration which has a number of members. The first member is the Constructor which has a body, which has its own set of statements 😅 … phew!

Thankfully, we don’t have to memorise all the different names of all the different types of children! TypeScript also gives us an easy way to iterate over all the child nodes of an AST node, with the forEachChild() function.

We can use forEachChild() to loop over the AST, and manually filter out the nodes until we get what we’re after:

TypeScript code that uses forEachChild to find a specific AST node.

That works well, and code just like this powers much of the TSLint project. But it isn’t particularly easy to read, or write, or maintain. And to even get started you have to know about the finer details of TypeScripts’ SourceFile APIs. We can do better!

We have a tree structure that we run queries against to select tree nodes. This is directly analogous to using CSS selectors to query the DOM and select elements!

Let’s look at the TSQuery code for doing the same thing:

Using TSQuery to find the same AST node with a lot less code!

That’s a bit better, isn’t it? No more createSourceFile(), and no more forEachChild()!

TSQuery replaces all the manual iterating and filtering of the earlier example with familiar CSS-like selectors. My hope is that, by using a familiar mental model, we can break down some barriers and enable more developers to build really useful tools for the TypeScript ecosystem.

TSQuery also makes it possible to compose, share, and manipulate AST selectors in ways that wouldn’t have really been possible before!

I hope you like it, and I can’t wait to see what people make with it!

What next?

Hopefully I’ve explained why this is a Good Thing™️, and your mind is bursting with great ways to use TSQuery!

I’m going to follow this post up with some examples of how TSQuery can be used, including:

  • Creating custom TSLint rules
  • Creating custom Angular Schematics
  • Finding out interesting stuff about your code base
  • AND MORE!? ⚡️️️️️️ ⚡️️️️️️ ⚡️️️️️️

Until then, please reach out with any questions, ideas, anything! ❤️

--

--