Topic 0.2
How TypeScript Works: Check, Then Erase
In one line
The TypeScript compiler, tsc, does two separate jobs: it type-checks your code and reports errors, and it emits plain JavaScript with every type removed. Types never exist while the program runs.
Think of it like this
Scaffolding on a building site. While the house is being built, scaffolding holds everything in place and stops mistakes. When the house is finished, the scaffolding is taken down: people live in the house, not in the scaffolding. Types are scaffolding. They guide and check you while you build, then they are removed and only the JavaScript is left to run.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Compiler
- A program that reads code in one form and writes it out in another.
tscturns TypeScript into JavaScript. - tsc
- The TypeScript Compiler, the command-line program that type-checks and emits JavaScript.
- Emit
- To write out the JavaScript files the compiler produces.
- Type erasure
- Removing all types when emitting, so the JavaScript that runs has no types in it.
- Compile time
- When the compiler checks the code, before it runs. Type errors happen here.
- Assignable
- Fits into. A value is assignable to a type if it keeps all of that type's promises.
- Error code
- The
TSnumber in an error, likeTS2345. Each kind of mistake has its own code you can search for. - --noEmit
- A tsc option that only checks types and writes no files.
Step by step
01Two jobs, one tool
When you run tsc, it parses your files, type-checks them, and emits JavaScript. Checking and emitting are separate steps: an error found while checking does not cancel emitting.
02What erasure looks like
Here is a real file and what tsc wrote for it in a fresh project (made with tsc --init, Topic 0.4). The annotations are gone; the rest is the same JavaScript you would write by hand.
The two extra lines at the top come from the project's settings: this project uses CommonJS modules (Node's older module format), and the last line points to a source map, a file that lets debuggers show your .ts lines.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet("Asha"));
//# sourceMappingURL=index.js.map03Errors and output together
Change the call to greet(42) and run tsc again. It prints the error and **still writes index.js**. In a terminal it prints a friendlier "pretty" form, with the line of code and a squiggle under the problem.
The exit code tells scripts what happened: 0 means no errors; 2 means errors were found but files were still written. With --noEmit and errors it is 1 (errors, nothing written). CI systems fail a build on any non-zero exit code.
04Reading a long error from the bottom
Some errors explain themselves in steps. Each indented line is one level deeper into *why* the outer line failed. Read the first line for where, and the deepest line for the real cause.
In the second example below, the first line says the function doesn't fit Handler; the next says the parameters text and amount don't match; the deepest says the real problem: a number can't go where a string is needed.
05What types can't see
Types describe your code, not the data that arrives while it runs. When you use as to tell TypeScript what some unchecked data is, you're making a promise the checker can't verify. The third example lies about JSON.parse output and crashes at runtime with a clean Types panel.
Try it yourself
- 1
Make the type exist at runtime
In the first example, replace the last line with
console.log("x" in p);. Predict: error or not? What prints?No error and
true:inchecks the object, which exists at runtime. Types are gone, values stay. - 2
Fix the long error two ways
In the second example, change
logTextto take(amount: number)and logamount * 2. The error disappears and1000prints.Undo that, and instead change
Handlerto(amount: string) => void. Now the error moves toonPay(500). Which fix is right depends on what the program means, and the checker can't decide that for you. - 3
Stop lying
In the third example, change
{ msg: string }to{ msg: number }. The Types panel now shows an error on theshout(data.msg)call, because the claim now matches the data and the mismatch is visible.
Code & diagrams
Point is a type, so it only exists for the checker. TypeScript reports the last line, and at runtime typeof Point is "undefined" because the type was erased.
Console
(nothing was printed)Expected console output
{ x: 3, y: 4 } object
undefinedEach indented line explains the line above it. The runtime crash in the console is exactly what the deepest line predicted.
Console
(nothing was printed)Expected console output
Uncaught TypeError: text.toUpperCase is not a functionNo type errors, yet it crashes. The as { msg: string } part is a claim TypeScript cannot check, because the data only exists at runtime.
Console
(nothing was printed)Expected console output
Uncaught TypeError: text.toUpperCase is not a functionBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a type as a value
In the first example, add const q = new Point(); at the end.
Myth vs fact
Myth
If tsc reports an error, no JavaScript is produced.
Fact
By default tsc emits JavaScript even with type errors (exit code 2). Use noEmitOnError or --noEmit plus a failing CI step to block bad code.
Myth
Types are checked while the program runs.
Fact
Types are erased. Nothing checks them at runtime; data from outside must be validated with real code.
Myth
as converts a value to another type.
Fact
as changes nothing at runtime. It only tells the checker to trust you. Converting needs real code like Number(text) or String(n).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
TypeScript's design goals explicitly say it should *not* add runtime behaviour or type-directed emit: the output is your code minus types. The few older features that do emit code (enums, namespaces, parameter properties) are why
erasableSyntaxOnly(TypeScript 5.8) exists, so Node's type stripping can run files without a compiler (Topic 12.5). - ▸
Exit codes are part of the contract:
0success,1diagnostics present with outputs skipped,2diagnostics present with outputs generated. Scripts that only check$? -eq 0already treat both error cases as failures. - ▸
Type checking cost grows with how complex types are, not with how much code runs. A slow build is almost always a few expensive types or too many files included;
--extendedDiagnostics(Topic 12.6) shows where the time goes.
Remember this
- 1
The problem this solves: Browsers and Node.js run JavaScript, not TypeScript. Without a step that checks types and then removes them, your typed code could not run anywhere. The TypeScript compiler is a program called **
tsc** (TypeScript Compiler). A compiler reads code in one form and produces another.tscreads.tsfiles and does two jobs: type checking (find every broken type promise and report it) and emitting (write out.jsfiles with the types taken out). The two jobs are independent: one can happen without the other. - 2
Type erasure means the emitted JavaScript contains no types at all.
function greet(name: string): stringbecomesfunction greet(name). Atype Point = { x: number; y: number }line disappears completely. This is why TypeScript can't change how your code behaves, why it adds no runtime cost, and why you can't ask about a type while the program runs:typeof Pointat runtime is just"undefined", becausePointis gone. - 3
Errors don't stop the JavaScript from being written, by default.
tscreports the error *and* still emits the.jsfile, because the JavaScript it would produce is perfectly valid; the type error is a warning about your intentions. That's why the live examples here still run when the Types panel is red. If you want a build to refuse to emit on errors, there's a setting for that,noEmitOnError(Topic 12.3), and most projects make CI fail whentscreports errors. - 4
An error line has a fixed shape:
index.ts(6,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.Read it left to right: the file, then (line, column) where the problem starts, then the error codeTS2345(each kind of mistake has a number you can search for), then the message. Long errors continue on indented lines that explain *why*, going one level deeper each time; the deepest line is usually the real cause. - 5
"Assignable" is the word you'll read most. Assignable means "fits into":
'string' is not assignable to 'number'means a string doesn't fit where a number was promised. Assigning to a variable, passing an argument, returning a value and putting an item in an array are all checked with this same "does it fit" question. - 6
Because types are erased, TypeScript trusts what you tell it about things it can't see.
JSON.parsereturns data nobody has checked; if you say "this is{ msg: string }" withas, the checker believes you, even if the real data hasmsg: 42. The program then crashes at runtime with no type error anywhere (see the third example). Checking real data at runtime is a separate job, done with code (Topic 13.4). - 7
The checker runs in more places than
tsc. Your editor runs it continuously as you type (that's where red squiggles come from), and many build tools (Vite, esbuild, Node's own TypeScript support) only erase types without checking them, for speed. In those setupstsc --noEmit(check only, write nothing) is run separately, usually in CI. Topic 12.5 covers every way to run TypeScript.
Explain it without notes
What does tsc do with a .ts file?
What is type erasure, and what are two consequences of it?
Does a type error stop JavaScript from being emitted?
Why can as lead to a runtime crash with no type error?
Practice
Run this through the checker in your head: which lines error, and what does the program print?
``ts
type Name = string;
const n: Name = "Ravi";
console.log(n.length);
console.log(typeof Name);
Rewrite this so a wrong runtime value is detected instead of trusted:
``ts
const settings = JSON.parse('{ "volume": "loud" }') as { volume: number };
console.log(settings.volume + 1);
Trade-offs
- ↔
Erasure keeps TypeScript fast and predictable, but means you pay twice at the boundaries: types for the checker *and* validation code for runtime. Schema libraries (Topic 13.4) let you write the rule once and get both.
- ↔
Emitting despite errors is convenient while you experiment and dangerous in production builds. Make CI the gate.
Done when you can
I can name the two jobs of
tscand say they are independent.I can explain type erasure and why
typeof SomeTypeis an error.I can read an error line: file, line, column, code, message.
I can read a multi-line error from the deepest line up.
I can explain why
ascan hide a runtime crash.