Add Type Safety and Prevent Runtime Errors With AST
See how types can surface runtime errors
This lesson preview is part of the Practical Abstract Syntax Trees course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Practical Abstract Syntax Trees, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:12] In the previous lesson, the initial script for logging the numeric values worked for one specific code snippet. Updating that to another code snippet resulted in a runtime error because the underlying AST changed.
[00:13 - 00:24] The first step we took was to use the Babel traverse package, which allowed us to more generically visit nodes within the tree. Ideally, this issue with the initial script would have been caught before the script was even ran.
[00:25 - 00:34] Fortunately, many of these issues can be caught in your editor or at compile time with TypeScript. The type definitions define all of the nodes in each node's specific properties .
[00:35 - 00:46] This means only valid properties can be accessed. Additionally, since many properties can be polymorphic or point to many other types of nodes, it requires narrowing the type to only the nodes with that property defined.
[00:47 - 01:00] This results in many more runtime checks and an overall more robust script leading to fewer runtime errors. You can think of the AST that the type definitions represent as the most generic AST, since it represents every possible node in property.
[01:01 - 01:23] Keep in mind that TypeScript is not a requirement for any of this, but it does help avoid runtime errors and provides code completion, which makes it easier to work with complex nodes, so the rest of this course will continue to use TypeScript for our scripts. Let's start by renaming the original parser.js file to parser.ts and convert the requirement to an import statement.
[01:24 - 01:42] Now we have a parser that's written in TypeScript, but need a way to execute it . There are a few options, including running babble on parser.ts and transpiling it to JavaScript.
[01:43 - 01:50] A simpler approach is to execute the script with TS node. This package can be used in the same way as node, but it offers TypeScript support.
[01:51 - 02:09] Let's go ahead and install the TS node in TypeScript packages as development dependencies. Now that we have the packages installed in this script converted to TypeScript, we can now run the script with TS node.
[02:10 - 02:19] There are two things to call out here. First, note that we executed TS node using NPX. This will be used interchangeably throughout the course.
[02:20 - 02:32] Secondly, the script isn't executed because there are type errors. The babble parser package includes its own TypeScript type definitions, which is why nothing new needed to be installed, but we're now seeing type errors.
[02:33 - 02:39] If we switch back to our editor, we can also see these type errors. So, what do these type errors mean and how can we fix them?
[02:40 - 02:49] Let's take a closer look at each of the type errors. Each of them says something along the lines of property expression does not exist on Type statement, but what is Type statement?
[02:50 - 03:01] We can answer this by inspecting the type definitions for babble parser. As we can see, the type definitions for the parse function are defined in the current project's node modules.
[03:02 - 03:10] These types then reference babble types type definitions. This package will be covered more in depth in a later module.
[03:11 - 03:18] This file interface represents the return type of the babble parser parse function. We can then inspect the program interface.
[03:19 - 03:28] We can see that the program's body property is an array with each element of type statement. We can continue by inspecting the type of statement.
[03:29 - 03:37] Now we can see that statement is a union of 47 possible different types. Looking at the errors again, you'll notice a more specific error message.
[03:38 - 03:50] Property expression does not exist on Type block statement. This error is trying to say that all elements from AST.program.body can be any one of the types from the statement type union declaration.
[03:51 - 04:03] This means in order to access expression, all 47 types must have an expression property. As we can see with this error, the first type in the union block statement does not have an expression property.
[04:04 - 04:11] However, this script isn't intended to work with these types of nodes. What we really want is to only handle very specific types of nodes.
[04:12 - 04:24] As mentioned, the types represent the most generic AST and exhaustively lists every possible type for each node in the tree. Let's fix the errors in the parser script by narrowing the types to only the nodes we are expecting to handle.
[04:25 - 04:34] First, let's grab the first statement from the program body. Then we can simplify the prints to use this new statement variable.
[04:35 - 04:59] As mentioned, statement can be 47 different types for this script, so we only really want to check for expression statements. Now we know it's an expression, so we can access the expression property.
[05:00 - 05:10] But we only care about expression statements that are also a binary expression. And more specifically, with a numeric value on the left.
[05:11 - 05:46] And another binary expression on the right. Where that binary expression also has a numeric value on the left.
[05:47 - 06:03] And on the right. Now, with these checks, all of the type errors have went away.
[06:04 - 06:13] Lastly, if you've changed the equation, make sure it's the original example. Now running this script should produce no type errors and print the same output as before.
[06:14 - 06:28] However, this time, if we were to change the equation, the script will now output nothing. This is because this large if statement will return false since the AST will have a different shape.
[06:29 - 06:39] Previously, these logs assumed the exact shape of the AST. Changing the source code, which changed the AST structure, resulted in a runtime error.
[06:40 - 06:48] The script still only prints the values for the specific AST. However, by adding these types and fixing those errors, our script is now more robust.
[06:49 - 06:59] It can now handle any AST and will avoid throwing a runtime error. As you traverse ASTs, this will likely come up again at different points in the tree since many nodes can be polymorphic.
[07:00 - 07:12] The previous lesson used the Babel traverse package to traverse any AST and print the numeric values for any string of code. We want this flexibility, but also this added type safety.
[07:13 - 07:23] The Babel parser package includes type definitions. This is why it was possible to rename parser.js to parser.ts, install Type Script tooling, and immediately get type errors.
[07:24 - 07:34] However, the Babel traverse package does not ship with its own type definitions . Fortunately, there are third-party type definitions available in the definitely typed repository.
[07:35 - 07:43] These types are all published under the types organization on MPM. The corresponding types can be installed with the types Babel traverse package.
[07:44 - 08:11] Let's again start by renaming traverse.js to traverse.ts and update the imports . Again, remember to switch back the code to the original example.
[08:12 - 08:26] Then we can install the types Babel traverse package. Then the script can be run.
[08:27 - 08:41] And we can see it prints the same output. For this specific example, the types aren't offering a huge advantage because we're traversing only numeric literal nodes, which are leaf nodes and have no child nodes.
[08:42 - 08:53] The types are more useful when working with larger or more complex nodes. However, it still improves code completion and catches simple things, like forgetting that a path is passed to the visitor callback and not a node.
[08:54 - 09:13] In practice, you'll likely need to work with these nodes higher in the tree, which can point to multiple different types of nodes. Adding types can be a great way to improve both the experience by adding better code completion and robustness by surfacing these types of unsafe AST navigation at compile time instead of runtime.
[09:14 - 09:29] The rest of this course will continue using TypeScript, but the types can always be ignored and the scripts written in JavaScript, if you prefer. Now that we have an understanding of some of the foundational AST concepts and tooling, the next module will cover practical examples of how ASTs can be applied.
[09:30 - 09:31] What could you apply to me?