-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
84: How the TypeScript Compiler Works: AST and Type Checker
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 isnumber. - Right side:
StringLiteral→ Type isstring.
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".
There are no comments for now.