Inside the TypeScript compiler
Follow a line of TypeScript through the scanner, parser, binder, checker, and emitter, and see how tsc and your editor share one compiler.
- 01Name the five partsSay what the scanner, parser, binder, checker, and emitter each take in and hand on.
- 02Read the compiler's workRead tokens, a syntax tree, and scopes, and tell which part of the compiler reported an error.
- 03Explain tsc and the editorDescribe a Program, and how tsc and your editor's language service use the same compiler in different ways.
Your code's trip through the compiler
Every lesson so far has shown you what TypeScript says about your code. This lesson shows how it works that out. Here is a small file. Point at any name to see its type:
const price: number = 3;const quantity = 2;const total = price * quantity;// ^?console.log(total);Line 1 makes price, a number. Line 2 makes quantity. Line 3 multiplies them, and the query on line 4 shows const total: number. Line 5 prints 6.
To get from this text to that answer, the TypeScript compiler (the program behind tsc and your editor) passes the file through five parts, one after another:
- The scanner reads the characters and groups them into tokens:
const,price,:,number, and so on. - The parser fits the tokens together into a syntax tree: line 1 is a variable declaration with a name, a type, and a value.
- The binder writes down every name the code declares, like
priceandtotal, and which part of the code each one belongs to. - The checker works out the type of every name and value, and reports the errors.
- The emitter writes the JavaScript, without the types.
You met the two big jobs, checking and emitting, in How TypeScript works. This lesson opens them up. Knowing the parts helps you read errors (which part is complaining?), understand why your editor answers so fast, and know what tools like linters and bundlers borrow from the compiler.
A story passes through many hands before it reaches the paper. A typist turns the notes into words. An editor makes sure the words form sentences. A clerk lists every person the story mentions, so "she" on page two can be traced to "Asha" on page one. A fact checker checks each claim. Only then does the press print it.
- In real life: A typist splits the reporter's notes into words
- In TypeScript: The scanner splits your code into tokens
- In real life: An editor checks that the words make proper sentences
- In TypeScript: The parser builds a syntax tree, or reports a syntax error
- In real life: A clerk lists every person the story names
- In TypeScript: The binder makes a symbol for every declared name
- In real life: A fact checker checks each claim against the list
- In TypeScript: The checker checks each value against its type
- In real life: The printing press prints the story
- In TypeScript: The emitter writes the JavaScript
The TypeScript compiler is five parts in a row. The scanner turns text into tokens, the parser turns tokens into a syntax tree, the binder gives every name a symbol, the checker works out types and reports errors, and the emitter writes JavaScript. tsc, your editor, and the checker on this page all run the same parts.
The scanner and the parser: from text to a tree
To the compiler, your file starts as one long string of characters. The scanner reads it from left to right and cuts it into tokens, the smallest pieces that mean something: a keyword like const, a name like price, a number like 3, or punctuation like : and ;. Spaces, line breaks, and comments separate tokens but aren't tokens themselves. The compiler calls them trivia. JavaScript engines read your code the same way (Lexical grammar covers JavaScript's tokens).
The parser asks the scanner for one token at a time and fits them into a tree. Each piece of the tree is a node, and each node has a kind, like VariableStatement or NumericLiteral (a number written in the code). This tree is called the abstract syntax tree, or AST. "Abstract" means it keeps the meaning and drops details like spaces and the ;. JavaScript engines build one too; Parsing & abstract syntax trees shows theirs.
The playground below runs the real compiler on one line you type. Pick 1. Scanner to see the tokens, then 2. Parser to see the tree. The other buttons are for the next sections, but try them too.
const price: number = 3;…Loading the compiler…
The scanner reads the characters from left to right and groups them into tokens: a keyword, a name, a colon, a number. Spaces and comments are skipped. Seven tokens, one statement in the tree, one name in the file's scope, and no errors. The emitter writes the same line without : number.
For const price: number = 3;, the scanner reads seven tokens: ConstKeyword, Identifier (a name), ColonToken, NumberKeyword, EqualsToken, NumericLiteral, and SemicolonToken. The parser turns them into the tree below. The const keyword doesn't get a node of its own: the parser writes "this is a const" on the declaration list instead.
const price: number = 3;. The statement holds a list of declarations, the list holds one declaration, and the declaration has three parts: the name, the type, and the value.When the tokens don't fit, the parser reports a syntax error. Here is one, on purpose:
const cart = ["Tea", "Cake"];if (cart.length > 0 { console.log("Cart has items");}Line 1 makes a cart with two items. Line 2 starts an if but never closes the bracket around its condition. The parser says ')' expected: it expected a ) and found {. Notice the error's number, ts(1005). Errors numbered in the 1000s come mostly from the scanner and the parser.
The parser doesn't give up at the first mistake. It recovers: it acts as if the ) were there and keeps building, so the rest of the file still gets a tree, and the later parts still run. You can see this in what the emitter writes for the code above. The bracket you forgot is in it:
const cart = ["Tea", "Cake"];
if (cart.length > 0) {
console.log("Cart has items");
}Line 2 now reads if (cart.length > 0) {, and the code prints Cart has items. Don't rely on this: the parser's guess is only a guess, and a wrong guess can cause confusing errors further down. Fix syntax errors first.
The binder: a symbol for every name
A tree tells the compiler what each line looks like, but not what the names mean. If a file says discount in three places, are they the same discount? The binder answers that. It walks the tree once and, for every name the code declares, makes a symbol: one record for that name. Later, the checker connects each use of the name to its symbol.
It puts each symbol in a scope: the part of the code where the name exists. The file is a scope, each function is a scope, and a block in { } is a scope for let and const (the Scope lesson covers the JavaScript rules the binder follows). Here are two scopes with a discount each:
const discount = 10; function finalPrice(price: number) { const discount = 2; return price - discount; // ^?} console.log(finalPrice(20));console.log(discount);Line 1 declares a discount in the file's scope. Line 3 declares finalPrice, also in the file's scope, and its parameter price, which belongs to the function. Line 4 declares a second discount, in the function's scope. The binder gives each discount its own symbol.
Line 5 uses discount. To find out which one, the compiler looks in the closest scope first, the function, and finds the one from line 4. The query on line 6 shows const discount: 2: the inner one, with its exact value as its type because a const can never change. Line 9 prints 18 (20 minus 2), and line 10, outside the function, prints the outer 10.
finalPrice. Line 5 looks in the closest scope first, so its discount is the inner one, 2.The binder also notices when one scope declares the same name twice. This is an error on purpose:
let total = 0;let total = 5;console.log(total);Lines 1 and 2 both declare let total in the file's scope. When the binder adds the second one to the file's table of names, it finds the first one already there and reports Cannot redeclare block-scoped variable 'total' at both lines. "Block-scoped" means a let or const. JavaScript agrees: running this code stops with a SyntaxError.
The binder never asks what type anything is. It only records which names exist and where. That makes it fast, and it means the checker, the next part, never has to search the whole tree for a name.
The checker and the emitter
The checker is the biggest part of the compiler, and it does the work you have seen in every lesson. For each name, it follows the symbol to its declaration and works out its type. For each value, it compares the value's type with the type the code says it should have:
type CartItem = { name: string; price: number }; const tea: CartItem = { name: "Tea", price: 3 };const label = tea.name.toUpperCase();// ^?const cost: string = tea.price;Line 1 names a type, CartItem. Line 3 makes tea a CartItem, and the checker confirms the object has a text name and a number price. Line 4 calls toUpperCase on tea.name: the checker follows tea to its symbol, finds the type CartItem, finds that name is a string, and finds that strings have a toUpperCase that gives back a string. The query on line 5 shows the result: const label: string.
Line 6 says cost must be a string and gives it tea.price, a number. The checker reports Type 'number' is not assignable to type 'string', on purpose here: a number is going where text has to go.
The checker is lazy: it works out a type only when someone asks for it. When tsc checks a file, it asks about everything. When you point at one name in your editor, it asks about that one name, and the answer comes back fast.
The emitter comes last. It walks the syntax tree and prints it back out as JavaScript, leaving out every type, interface, and type alias:
type CartItem = { name: string; price: number }; // Tea is the cheapest item.const tea: CartItem = { name: "Tea", price: 3 };console.log(tea.price * 2);// Tea is the cheapest item.
const tea = { name: "Tea", price: 3 };
console.log(tea.price * 2);The type alias on line 1 is gone, and so is the blank line after it. The comment stays, because the emitter keeps comments unless you ask it not to. : CartItem is gone from the tea line. Both versions print 6. The emitter can also write older JavaScript for older browsers, and .d.ts files that describe your code's types; tsconfig: output covers the options.
The table below sums up the five parts. Read each row as: this part, what it reads, what it makes, and the errors it reports.
| The part | What it reads | What it makes | Errors it reports |
|---|---|---|---|
| Scanner | The characters of one file. | Tokens: keywords, names, numbers, and punctuation. | A few, like text that is never closed: Unterminated string literal. |
| Parser | The tokens. | A syntax tree for the file, called the AST. | Syntax errors, like ')' expected, mostly numbered in the 1000s. |
| Binder | The syntax tree. | Symbols: one per declared name, kept in a table for each scope. | A name declared twice in one scope, like Cannot redeclare block-scoped variable. |
| Checker | The trees and symbols of every file. | The type of each name and expression. | Type errors, like Type 'string' is not assignable to type 'number', mostly numbered 2000 and up. |
| Emitter | The syntax tree. | JavaScript, and .d.ts files when you ask for them. | Almost none of its own. It writes the file even when other parts found errors. |
One Program, two drivers: tsc and your editor
Real projects have many files, and they import from each other. The compiler gathers them all into one Program: a SourceFile (one file's text and syntax tree) for every file in the project, every library file TypeScript adds (like lib.dom.d.ts, which describes the browser), and the compiler options. The checker works on the whole Program, so it can follow an import into another file:
export function formatPrice(cents: number) { return `$${(cents / 100).toFixed(2)}`;}import { formatPrice } from "./price.ts"; const label = formatPrice(450);// ^?console.log(label);price.ts exports a formatPrice function that turns cents into dollars. In cart.ts, line 1 imports it, and line 3 calls it with 450. The query on line 4 shows const label: string: the checker followed the import to price.ts's SourceFile and worked out what formatPrice gives back. Line 5 prints $4.50.
tsc builds it, checks every file, writes JavaScript, and stops. Your editor keeps it open and asks it questions as you type.Two tools drive the compiler, in different ways:
tsc, the command, builds the Program once, checks every file, writes the JavaScript, and stops.- Your editor talks to TypeScript's language service: the same compiler, wrapped so it can answer questions. In VS Code it runs in a separate process called
tsserver. The editor asks "what are the errors in this file?", "what type is at this spot?", "what could come after this dot?". When you change a file, the language service builds a new Program for the next question, reuses the trees of the files you didn't change, and asks the checker only what the question needs.
The table below compares the two. Read each row as a question, then how each one answers it.
| Question | tsc | Your editor's language service |
|---|---|---|
| When does it run? | When you type tsc in a terminal or a build script runs it. | All the time, while the file is open. |
| How much does it check? | Every file in the Program, then it stops. | Mostly the files you have open, and only what each question needs. |
| What does it give you? | A list of errors in the terminal, and JavaScript files. | Red underlines, hovers, completions, rename, and go to definition. |
| Does it keep the Program? | No. The next run starts again, unless you use --watch. | Yes. After each change, it builds a new Program and reuses the trees of the files you didn't change. |
The checker on this page is a language service too. When a TypeScript example comes near the screen, the page starts a Web Worker (a script that runs beside the page, so typing never waits for it), downloads the real compiler, version 5.9.2, and the library files it needs, and asks it for errors and // ^? answers. The playground in this lesson asks the same compiler for its tokens, tree, and scopes. Edit & run is different: to run your code it only removes the types, with a faster tool that never checks them (TypeScript tooling explains this split).
The compiler this lesson shows is written in TypeScript and runs as JavaScript. In March 2025 the TypeScript team announced a port to the Go language, codenamed "Corsa". It shipped as TypeScript 7.0 on July 8, 2026, about 8 to 12 times faster on full builds. It is a port, not a new design: the same parts and the same checking rules as TypeScript 6.0, the last version written in TypeScript. (6.0 changed some defaults, like turning strict on, and 7.0 keeps them.) Other tools, like this page's checker, call the compiler from JavaScript through its programming interface. TypeScript 7.0 has none yet; 7.1, in beta since October 20, 2026, adds a new and different one. So this page runs TypeScript 5.9, written in TypeScript. (Checked against the TypeScript team's announcements in October 2026.)
A broken checkout, one part at a time
Here is a checkout script with two mistakes, from two different parts of the compiler. It reads a quantity from a text box (Form elements covers .value) and lets the shopper pay when the quantity is above 0. Press each button and watch the list under the code:
const quantityInput = document.createElement("input");quantityInput.value = "2"; function readQuantity(input: HTMLInputElement): number { return input.value;} if (readQuantity(quantityInput) > 0 { console.log("Ready to pay");}can't run: line 8 isn't valid JavaScript1 error: line 81 error: line 5Two errors from two parts of the compiler. Line 8 is missing its ), so the parser reports ')' expected. Line 5 gives back text where the function promised a number, so the checker reports Type 'string' is not assignable to type 'number'. The parser filled in the bracket and carried on, which is why the checker could still find line 5.
With Both mistakes, line 8 gets ')' expected from the parser, and line 5 gets Type 'string' is not assignable to type 'number' from the checker: a text box's .value is always text, and line 4 promised a number. Both errors show at once because the parser recovered. Fix the bracket leaves the checker's error, and the code runs and prints Ready to pay by luck. Fix the type leaves the parser's error, and the code can't run at all. Fix both clears the list.
When you see a long list of errors, look for the 1000s first. A syntax error can confuse the parser's guess about everything after it, and fixing it often makes other errors disappear.
const total = ;if (true { }const label = "Tea;let size = "M"; let size = "L";class Cart {} class Cart {}const price: number = "3";console.log(totl);const tax = 2; tax = 3;
Sort each line by the part of the compiler that reports its error. Each line is checked on its own.
Common mix-ups
- "TypeScript runs my code to find the errors." No. No part of the compiler runs your code. The checker reads the tree and the symbols and reasons about types.
- "A syntax error stops the checker." No. The parser recovers and builds a whole tree, so the checker still runs. The checkout showed a parser error and a checker error at once.
- "The emitter needs the types to be right." No. It writes JavaScript from the tree even when the checker reports errors, unless you turn on
noEmitOnError. - "My editor has its own, simpler type checker." No. It asks TypeScript's language service, which uses the same compiler as
tsc. That is why the editor andtscreport the same errors. - "The syntax tree is just my text split into lines." No. It is a tree of nodes with kinds, like
VariableDeclaration. Spaces and comments aren't nodes; each node only remembers where it starts and ends in the text. - "TypeScript 7 checks my code with new rules." No. It is the same compiler ported to Go. It is faster, and it checks code the same way as TypeScript 6.0, whose new defaults it keeps.
Practice exercises
Use the playground in the scanner and parser section to check your answers: type a starter's line there and pick a part. Press Edit & run on code to see its errors and what it prints.
How many tokens does the scanner read in this line?
let quantity = 2;// let, quantity, =, 2, ;let quantity = 2;The scanner reads five tokens: let (LetKeyword), quantity (Identifier), = (EqualsToken), 2 (NumericLiteral), and ; (SemicolonToken). The spaces between them are trivia.
This line has an error. Which part of the compiler reports it?
const total = 4 *;const total = 4 * 2;The parser expects a value after * and finds ;, so it reports Expression expected (1109), an error in the 1000s. Giving * a second number fixes it.
The parser marks line 3. Fix it. What does the fixed code print?
const prices = [3, 4];let total = 0;for (const price of prices { total += price;}console.log(total);const prices = [3, 4];let total = 0;for (const price of prices) { total += price;}console.log(total);Line 3 needs ) to close for (const price of prices. With it, the loop adds 3 and 4, and line 6 prints 7.
What does this print? Decide which discount line 5 uses before you run it.
const discount = 5; function checkout(total: number) { const discount = 1; return total - discount;} console.log(checkout(10));const discount = 5; function checkout(total: number) { const discount = 1; return total - discount; // ^?} console.log(checkout(10));Line 5 is inside checkout, so the closest discount is the one from line 4, which is 1. The query on line 6 shows const discount: 1, and the code prints 10 minus 1, which is 9.
In the parser's tree, what kind of node is the whole of line 2, cart.push("Tea");?
const cart: string[] = [];cart.push("Tea");console.log(cart.length);const cart: string[] = [];// ExpressionStatement > CallExpression > PropertyAccessExpressioncart.push("Tea");console.log(cart.length);Line 2 is an ExpressionStatement: a statement made of one expression. Under it is a CallExpression (the call), and under that a PropertyAccessExpression (cart.push) and the StringLiteral "Tea".
How many lines of JavaScript does the emitter write for this file?
type Size = "small" | "large";interface Order { size: Size;}const order: Order = { size: "large" };console.log(order.size);// The emitter writes only these two lines:const order = { size: "large" };console.log(order.size);The type alias on line 1 and the interface on lines 2 to 4 are types only, so the emitter leaves them out. Line 5 loses : Order. The emitter writes two lines, and they print large.
Check your understanding
For each question, think about which part of the compiler does the work. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7In what order do the parts of the compiler work on your code?
Choose an answer to see the explanation.
Question 2 of 7What does the scanner hand to the parser?
Choose an answer to see the explanation.
Question 3 of 7Which part of the compiler reports the error in this code?
Read the code, then predictconst ready = true; if (ready { console.log("Go"); }Choose an answer to see the explanation.
Question 4 of 7These two lines get the same error twice. Which part reports it?
Read the code, then predictlet size = "M"; let size = "L";Choose an answer to see the explanation.
Question 5 of 7You point at a name in VS Code and see its type. What answered?
Choose an answer to see the explanation.
Question 6 of 7What is a Program in the TypeScript compiler?
Choose an answer to see the explanation.
Question 7 of 7Does tsc still write JavaScript for this file, which has a type error?
Read the code, then predictconst price: number = "3"; console.log(price);Choose an answer to see the explanation.
Key takeaways
- The scanner turns your text into tokens, skipping spaces and comments.
- The parser builds a syntax tree (the AST) from the tokens, reports syntax errors, and recovers so the rest still runs.
- The binder makes a symbol for every declared name and puts it in a scope; the closest scope wins.
- The checker works out types lazily and reports type errors. The emitter writes JavaScript from the tree, even when there are errors.
- A Program is every file plus the options.
tscchecks it once; your editor's language service keeps it and answers questions. - TypeScript 7 is the same compiler ported to Go: faster, and it checks code the same way as TypeScript 6.0.
Remember the one-liner.
Text becomes tokens, tokens become a tree, names become symbols, the checker finds the types, and the emitter writes the JavaScript.
Coming next: How inference works. It looks inside the checker at how it works out a type you never wrote.