Command Palette

Search for a command to run...

Hectal
PHASE 0Beginner ~20 min· topic 2 of 4

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. tsc turns 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 TS number in an error, like TS2345. 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.

Two jobs, one tooldiagram
Rendering diagram…

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.

index.js (emitted)whole filejs
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function greet(name) {
    return `Hello, ${name}!`;
}
console.log(greet("Asha"));
//# sourceMappingURL=index.js.map

03Errors 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.

terminal
$ npx tsc
── expected output ──
index.ts:5:19 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
 
5 console.log(greet(42));
~~
 
 
Found 1 error in index.ts:5

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. 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: in checks the object, which exists at runtime. Types are gone, values stay.

  2. 2

    Fix the long error two ways

    In the second example, change logText to take (amount: number) and log amount * 2. The error disappears and 1000 prints.

    Undo that, and instead change Handler to (amount: string) => void. Now the error moves to onPay(500). Which fix is right depends on what the program means, and the checker can't decide that for you.

  3. 3

    Stop lying

    In the third example, change { msg: string } to { msg: number }. The Types panel now shows an error on the shout(data.msg) call, because the claim now matches the data and the mismatch is visible.

Code & diagrams

Types vanish when the code runs

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
undefined
A long error: read the deepest line

Each 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 function
TypeScript believes what you tell it

No 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 function

Break 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.

terminal
$ Types panel
── what you'll see ──
index.ts(5,20): error TS2693: 'Point' only refers to a type, but is being used as a value here.
index.ts(6,15): error TS2693: 'Point' only refers to a type, but is being used as a value here.

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: 0 success, 1 diagnostics present with outputs skipped, 2 diagnostics present with outputs generated. Scripts that only check $? -eq 0 already 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. 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. tsc reads .ts files and does two jobs: type checking (find every broken type promise and report it) and emitting (write out .js files with the types taken out). The two jobs are independent: one can happen without the other.

  2. 2

    Type erasure means the emitted JavaScript contains no types at all. function greet(name: string): string becomes function greet(name). A type 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 Point at runtime is just "undefined", because Point is gone.

  3. 3

    Errors don't stop the JavaScript from being written, by default. tsc reports the error *and* still emits the .js file, 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 when tsc reports errors.

  4. 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 code TS2345 (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. 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. 6

    Because types are erased, TypeScript trusts what you tell it about things it can't see. JSON.parse returns data nobody has checked; if you say "this is { msg: string }" with as, the checker believes you, even if the real data has msg: 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. 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 setups tsc --noEmit (check only, write nothing) is run separately, usually in CI. Topic 12.5 covers every way to run TypeScript.

Explain it without notes

01

What does tsc do with a .ts file?

02

What is type erasure, and what are two consequences of it?

03

Does a type error stop JavaScript from being emitted?

04

Why can as lead to a runtime crash with no type error?

Practice

01

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);

02

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 tsc and say they are independent.

  • I can explain type erasure and why typeof SomeType is 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 as can hide a runtime crash.