Skip to Content
Course content

84: How the TypeScript Compiler Works: AST and Type Checker

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, we treat the TypeScript compiler (tsc) as a magical black box. You write some code, it screams at you with a red squiggly line, you fix it, and it disappears. But if you want to truly master the language—especially when you're fighting with complex generics or conditional types—you need to understand what's happening under the hood. Specifically, you need to understand the transition from AST to Type Checking.

To make this concrete, let's imagine we're building a tiny, simplified version of a compiler. We aren't going to write 100,000 lines of C++, but we are going to walk through how the compiler sees a single line of code: const score = 10 + "5";

Turning Text into a Tree

The compiler doesn't "read" your code like a book. First, it breaks the text into tokens (Lexing), and then it organizes those tokens into an Abstract Syntax Tree (AST). The AST is just a giant JSON-like object that represents the structure of your program.

For our line const score = 10 + "5";, the AST looks roughly like this in the compiler's head:

{
  type: "VariableDeclaration",
  identifier: { name: "score" },
  initializer: {
    type: "BinaryExpression",
    operator: "+",
    left: { type: "NumericLiteral", value: 10 },
    right: { type: "StringLiteral", value: "5" }
  }
}

Notice how the "meaning" is now structured. The compiler doesn't see a string of characters anymore; it sees a VariableDeclaration that contains a BinaryExpression. This structure is what allows TypeScript to perform analysis without getting confused by whitespace or comments.

The Type Checker's Walk

Once the AST is built, the Type Checker steps in. It "walks" the tree. It doesn't care about the variable name "score" yet; it cares about the initializer. It sees the BinaryExpression and asks: "What are the types of the left and right sides?"

  • Left side: NumericLiteral → Type is number.
  • Right side: StringLiteral → Type is string.

Now the Type Checker looks up the rules for the + operator. In JavaScript, number + string is perfectly legal (it results in a string). So, the Type Checker decides the resulting type of the BinaryExpression is string. It then assigns that type to the variable score.

Wait, I Forgot About Strict Nulls

Here is where I usually trip up when explaining this: I often forget that the Type Checker isn't just looking at the current node, but also the global context. Let's say I changed the code to const score = someValue + 10; where someValue is defined elsewhere as number | null.

If my simulated type checker just saw BinaryExpression(left, right), it might just say "Yep, number plus number is fine." But that's a bug. I have to remember that the Type Checker must perform a control flow analysis. It checks if someValue has been narrowed. If there's no if (someValue !== null) check before this line, the Type Checker throws the famous: "Object is possibly 'null'."

I've made this mistake in my own tool-building projects—forgetting that the AST is static, but the Type Checker's logic must be dynamic based on the path the code takes.

The Final Emission

Once the Type Checker is happy (or has finished listing all your sins), the compiler moves to the Emitter. This is the part that actually writes the .js file. Since the AST already knows that const score = 10 + "5" is just a variable assignment, the emitter simply strips away the type annotations (which don't exist in our example, but would if we had const score: string = ...) and prints the JavaScript.

The most important takeaway? The AST is the map, and the Type Checker is the inspector walking that map to make sure you haven't built a bridge to nowhere.




📋 Practical Task

Exercise: Mapping a Binary Expression AST

Imagine you are writing a simplified TypeScript Type Checker. You are given the following mock AST object representing the expression: const result = (5 * 2) + 10;

const mockAST = {
  type: "VariableDeclaration",
  identifier: "result",
  initializer: {
    type: "BinaryExpression",
    operator: "+",
    left: {
      type: "BinaryExpression",
      operator: "*",
      left: { type: "NumericLiteral", value: 5 },
      right: { type: "NumericLiteral", value: 2 }
    },
    right: { type: "NumericLiteral", value: 10 }
  }
};

Your Task: Write a short pseudo-code function or a JavaScript function called resolveType(node) that recursively traverses this specific AST structure. If the node is a NumericLiteral, return "number". If it is a BinaryExpression, check the types of its left and right children. If both are "number", return "number".

Demonstrate that your function correctly identifies the type of the initializer as "number".

Rating
0 0

There are no comments for now.

to be the first to leave a comment.