TypeScript Compiler Internals: From AST to Type Erasure

A deep technical look into the TypeScript compiler (tsc). Understand the parsing phase, Abstract Syntax Trees (AST), structural typing, and why type erasure means your types don't exist at runtime.

TypeScript has revolutionized frontend and Node.js development by bringing static type safety to a highly dynamic language. However, a fundamental misunderstanding plagues many developers: the belief that TypeScript types exist at runtime. They do not.

To truly master TypeScript, you must understand its compiler (tsc). Unlike the Java or C# compilers, which emit typed bytecode, the TypeScript compiler is fundamentally a transpiler designed with one ultimate goal: Type Erasure.

Phase 1: Parsing and the AST

The compilation process begins with the Scanner and Parser. The scanner reads the raw .ts source code text and converts it into a stream of tokens (e.g., Keyword(const), Identifier(x), EqualsToken). The parser then consumes these tokens to construct an Abstract Syntax Tree (AST).

The AST is an in-memory tree representation of your code's syntactic structure. Tools like Prettier and ESLint also rely on the AST to format or lint code. In TypeScript's AST, nodes contain metadata about both the JavaScript syntax and the TypeScript-specific type annotations.

Phase 2: The Binder and Symbols

Once the AST is built, the Binder traverses the tree to resolve scopes and create Symbols. A symbol connects a declaration (like let x = 10;) with every usage of x in that scope.

The binder is responsible for knowing that the x inside a specific function is different from the x in the global scope. It builds a symbol table that the next phase relies on heavily to track exactly which variables are being referenced at any given point.

Phase 3: The Checker (Structural Typing)

The Checker is the heart of the TypeScript compiler. It is a massive block of logic (over 40,000 lines of code in checker.ts) that traverses the AST and verifies that the types align according to the rules of Structural Typing.

Unlike languages like Java which use Nominal Typing (where a class Dog is only a Dog if explicitly declared as such), TypeScript uses Structural Typing (duck typing). If an object has the same shape—the same properties and methods—as an interface, the Checker considers it a match, regardless of its explicit name.

Phase 4: Emit and Type Erasure

If the Checker finds no errors (or even if it does, depending on your tsconfig.json), the Emitter takes over. The Emitter's primary job is to strip away every single TypeScript-specific syntax node from the AST.

Interfaces, Type Aliases, as assertions, generic parameters (<T>)—all of these are completely deleted. The resulting AST is pure JavaScript, which is then serialized back into text. This process is called Type Erasure. The emitted .js file has absolutely no concept of the types you so carefully defined.

Nominal vs. Structural Typing

Because TypeScript uses structural typing, developers often encounter surprising behavior. For instance, an empty interface interface Empty {} is compatible with almost everything in TypeScript. Any object you pass to a function expecting Empty will be accepted because every object structurally satisfies "having no required properties."

To simulate nominal typing in TypeScript, developers use techniques like "Branded Types" (intersecting a primitive type with a unique symbol) to force the Checker to reject structurally similar but conceptually distinct values (like mixing up a UserId string and a PostId string).

The Need for Runtime Validation

The reality of Type Erasure means that fetch() responses from an API cannot be magically validated by casting them: const data = await res.json() as User;. The as User assertion is erased at compile time. At runtime, if the API returns a malformed object, your app will crash when it tries to access missing properties.

For the boundary between your typed code and the untyped external world (APIs, LocalStorage, User Input), you must use runtime validation libraries like Zod or Joi. These libraries parse the incoming data and provide a type-guard that the TypeScript Checker respects.

Optimizing Compiler Performance

As projects grow, the Checker can become incredibly slow, primarily due to deep nested generics, complex union types, or excessive use of mapped types. Diagnosing these performance issues involves generating compiler trace files (--generateTrace) and analyzing them in Chrome DevTools.

Understanding the internals of tsc not only helps you write more performant types but also demystifies the illusion of compile-time safety, encouraging defensive programming at system boundaries.

Karuvigal Team
KT

Karuvigal Team

Building developer tools that save time and improve productivity.

Published on 26 ஜூன், 2026 11 min

Last updated: 26 ஜூன், 2026 Author Karuvigal Team