loading...

Easier TypeScript tooling with TSQuery

phenomnominal profile image Craig ☠️💀👻 ・6 min read

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:

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:

tsquery(ast, 'ClassDeclaration[name.name="MyClass"] > Constructor');

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 ~5 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:

  • The integrations for various IDEs (such as Webstorm, or VS Code) that make refactoring super easy

  • Specialised tools like TSLint for automated code-style checking

  • The whole suite of tools that power the Angular CLI for code generation and automatic updating

  • 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:

export class MyClass {
    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:

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 parentchild 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:

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:

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! ❤️

Discussion

pic
Editor guide