cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

How TypeScript works

Follow a .ts file through the TypeScript compiler: how it checks types, writes plain JavaScript, still writes it when there are errors, and what it can't check.

By the end, you can
  • 01
    Name the compiler's two jobsExplain how tsc checks your types and then writes plain JavaScript without them.
  • 02
    Predict what runsSay what tsc writes for a file, why types never change what the code does, and why code with errors still runs.
  • 03
    Pick the right checkTell which mistakes a static check catches before the code runs and which need a runtime check while it runs.

One command, two jobs

In the last lesson you saw TypeScript mark a mistake while you typed, and you saw that the browser runs plain JavaScript. This lesson follows one file through that process, step by step. Here is the price bug again, saved in a file called cart.ts:

cart.ts, with a price that is text (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const teaPrice: number = 3;const cakePrice: number = "4";console.log(teaPrice + cakePrice);

Line 1 says teaPrice is a number and gives it 3. Line 2 says cakePrice is a number too, but gives it the text "4". Line 3 adds the two and prints the result. The error on line 2 is on purpose: it's the bug this lesson follows.

The program that turns TypeScript into JavaScript is the TypeScript compiler. Its command is tsc (the next lesson installs it). A compiler is a program that reads code in one form and writes it in another. You give it the file name in a terminal, the window where you type commands:

Compile cart.ts in a terminalShell
tsc --target es2022 cart.ts

tsc is the command, and cart.ts is the file to compile. --target es2022 asks for modern JavaScript, so the output looks like the code you wrote; the emitting section shows what happens without it. Here is what tsc prints:

What tsc prints in the terminalText
cart.ts:2:7 - error TS2322: Type 'string' is not assignable to type 'number'.

2 const cakePrice: number = "4";
        ~~~~~~~~~

Found 1 error in cart.ts:2

Read the terminal output one line at a time:

  • Line 1 names the file, line, and column (cart.ts:2:7 means line 2, character 7), then the error number TS2322 and the message: text is going where a number has to go.
  • Lines 3 and 4 copy line 2 of your code and draw ~~~~~~~~~ under the name the error is about.
  • The last line counts the errors and says where they are.

That's the first job: checking. But tsc also did a second job, quietly. A new file appeared next to cart.ts:

cart.js, the file tsc writesPop out in the code editor (opens in a new tab)JavaScript
const teaPrice = 3;
const cakePrice = "4";
console.log(teaPrice + cakePrice);

This is the second job, called emitting: writing JavaScript. Lines 1 and 2 have lost their : number. Everything else is your code, exactly as you wrote it, error and all. Press Edit & run and it prints 34, the same bug as before.

A file cart.ts holding cakePrice: number = "4" goes into tsc. Two arrows come out. Arrow 1, check, leads to a terminal showing error TS2322 on line 2. Arrow 2, emit, leads to a file cart.js holding cakePrice = "4" with the type removed.cart.tscakePrice:number = "4"you write thistsc1. checkterminalerror TS2322on line 22. emitcart.jscakePrice = "4"no types left
One tsc command does two jobs with cart.ts. It checks the types and prints the error on line 2. It also writes cart.js, the same code without the types.
Real-life analogyA proofreader and a printer

A small print shop gets your draft. A proofreader reads it and circles mistakes. Then the printer prints the pages, without the notes in the margin. Unless you say "wait until it's fixed", the printer prints whatever is on the page, circled mistakes and all.

In real life: Your draft, with notes in the margin
In TypeScript: cart.ts, with types
In real life: The proofreader marks mistakes
In TypeScript: Checking: tsc reports errors
In real life: The printer prints the pages without the margin notes
In TypeScript: Emitting: tsc writes cart.js without the types
In real life: The printer prints even if a mistake is still marked
In TypeScript: cart.js is written even when there are errors
The main idea

The TypeScript compiler does two separate jobs. Checking reads your code and reports type errors. Emitting writes plain JavaScript: your code with the types removed. Neither job runs your code. Something else runs the JavaScript, like a browser or Node.

Checking reads your code without running it

Checking is called a static check. "Static" means "without running": TypeScript only reads the text of your code. That's why it can check code that never runs at all:

A function that is never called (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function couponLabel(percent: number) {  return percent.toUpperCase();} console.log("Coupons are off today");

Line 1 declares a couponLabel function that takes a percent, a number. Line 2 calls toUpperCase on it, but numbers have no such method; it belongs to text. Line 5 prints a message, and nothing ever calls couponLabel.

TypeScript marks line 2 on purpose: Property 'toUpperCase' does not exist on type 'number'. In plain words: "numbers don't have toUpperCase". A property is a named thing a value has, like a method. Plain JavaScript can't notice this mistake until someone calls the function, maybe weeks later. When the code runs, it prints Coupons are off today, because line 2 never runs.

To check, TypeScript has to know the type of every value. Most of them it works out by reading, which is called inference. Here it works out a type through a whole map call, without running it:

TypeScript works out a type by readingPop out in the code editor (opens in a new tab)TypeScript
const cart = [  { name: "Tea", price: 3 },  { name: "Cake", price: 4 },];const prices = cart.map((item) => item.price);//    ^?console.log(prices.join(", "));

Lines 1 to 4 make a cart list with tea and cake. Line 5 uses map to make a new list of just the prices. The // ^? comment on line 6 asks TypeScript what type prices has, and the page shows its answer: const prices: number[]. number[] means "a list of numbers". TypeScript knew each item.price is a number, so the list map gives back must hold numbers. Line 7 prints 3, 4.

Emitting writes plain JavaScript

The second job takes the same file and writes JavaScript. It mostly removes things. Here is a typed function with a type alias, a name for a type made with the type keyword:

cart.ts, with a type alias and annotationsPop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; function lineTotal(item: CartItem, quantity: number): number {  return item.price * quantity;} const tea: CartItem = { name: "Tea", price: 3 };console.log(lineTotal(tea, 2));

Line 1 names a type, CartItem: an object with a name that is text and a price that is a number. Line 3 declares lineTotal, which takes a CartItem and a quantity, and says it gives back a number (the : number after the brackets). Line 4 multiplies. Line 7 makes a tea item, and line 8 prints 6. There are no errors. Here is the cart.js that tsc --target es2022 writes for it:

cart.js, exactly as tsc writes itPop out in the code editor (opens in a new tab)JavaScript
function lineTotal(item, quantity) {
    return item.price * quantity;
}
const tea = { name: "Tea", price: 3 };
console.log(lineTotal(tea, 2));
  • The type CartItem line is gone. A type alias is only for TypeScript, so nothing of it is left.
  • Line 1 has lost : CartItem, : number, and the : number after the brackets.
  • Line 4 has lost : CartItem. The object itself is copied as it is.
  • The empty lines are gone, and the body is indented with four spaces. That's only how tsc lays out its output.

It still prints 6. Removing types like this is called type erasure.

Left, cart.ts: type CartItem, then lineTotal(item: CartItem, quantity: number). An arrow marked tsc removes the types leads to cart.js on the right: lineTotal(item, quantity), and no CartItem line.cart.tstype CartItem = …lineTotal(item: CartItem,quantity: number)with typestscremoves typescart.jslineTotal(item,quantity)the same code, no types
The whole type CartItem line is removed. So are : CartItem and : number. The function itself is copied as it is.

The table below shows what happens to each kind of thing in a .ts file. Read each row: this part of your code, what emitting does to it, and what's left.

What emitting does to each part of your code
In cart.tsWhat tsc does with itWhat's left in cart.js
: number after a name, like quantity: numberIt's removed. Only quantity is left.function lineTotal(item, quantity)
A type alias, like type CartItem = { … }The whole line is removed.Nothing: the line is gone.
A value, like "4" or 3It's copied exactly as you wrote it.const cakePrice = "4";
A function call, like Number("4")It's copied. It's JavaScript, so it runs.const cakePrice = Number("4");

Emitting can do one more thing: rewrite new JavaScript as older JavaScript, for old browsers. This is called downleveling (the Transpilers & polyfills lesson explains it). That's what --target picks. Without it, tsc aims at ES5, the edition of JavaScript from 2009, so the overview's cart.js comes out like this:

cart.js when tsc aims at ES5Pop out in the code editor (opens in a new tab)JavaScript
var teaPrice = 3;
var cakePrice = "4";
console.log(teaPrice + cakePrice);

Each const became var, the older way to declare a variable. The program does the same thing and prints 34. The next lesson sets the target once, in a settings file, so you don't type it each time.

Types disappear when the code runs

Because emitting removes every type, no type exists while the code runs. You can see this with JavaScript's typeof, which looks at a value while the code runs and names its kind (the Checking types reliably lesson covers it):

Asking about a type while the code runs (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Price = number; const cakePrice: Price = 4;console.log(typeof cakePrice);console.log(typeof Price);

Line 1 names a type, Price, which means a number. Line 3 makes cakePrice a Price and gives it 4. Line 4 prints number: typeof sees the value 4, not the name Price.

Line 5 asks about Price itself, and TypeScript marks it on purpose: 'Price' only refers to a type, but is being used as a value here. In plain words: "Price is only a type, and types don't exist when the code runs". Run it anyway, and line 5 prints undefined, which is what typeof says about a name that doesn't exist. The type Price line was removed, so there's nothing called Price left.

So a type can never change what your code does. The code that runs is the same with or without it. Try it: each button changes line 2. Watch the errors, the line tsc writes, and the output.

Playground: change the type, then the value
Tea and cake pricesPop out in the code editor (opens in a new tab)TypeScript
const teaPrice: number = 3;const cakePrice: number = "4";console.log(teaPrice + cakePrice);
Console output34
What happens
TypeScript1 error: line 2
Line 2 of cart.jsconst cakePrice = "4";
Try it yourself

The type says number and the value is text, so TypeScript marks line 2: Type 'string' is not assignable to type 'number'. tsc still writes const cakePrice = "4";, and the code prints 34.

TypeScript checks the code for real. The line tsc writes and the output are what the real compiler and the code produce. Each button changes only line 2. Reset goes back to the first button.

The first three buttons give three different types, or none. TypeScript's answer changes each time, but tsc writes the same line, const cakePrice = "4";, so the code prints 34 every time. Only the last button changes the value, to the number 4, and only then does the output change, to 7. A type describes a value. It never turns one kind of value into another.

Errors don't stop the output

You saw it in the overview: tsc reported an error and still wrote cart.js. That's on purpose. The two jobs are separate, and by default an error in one doesn't stop the other. The TypeScript team chose this because you often know better than the checker for a moment, like halfway through turning a JavaScript project into TypeScript.

When you'd rather have no JavaScript than JavaScript with a known mistake, add the --noEmitOnError option. An option (or flag) is a setting you add to a command, starting with --. With this one, any error means tsc writes nothing. The playground also shows each command's exit code: a number a command hands back when it finishes. Build scripts read it: 0 means success, and anything else means something went wrong. Try both prices with both commands:

Playground: build cart.ts with tsc
const teaPrice: number = 3;const cakePrice: number = "4";console.log(teaPrice + cakePrice);
cart.jswritten
What tsc does
TerminalFound 1 error in cart.ts:2
Exit code2
Try it yourself

tsc reports the error on line 2 and still writes cart.js. Exit code 2 means "errors, but the files were written". The page will print 34.

The command is tsc --target es2022 cart.ts. What tsc prints, writes, and returns is what the real compiler does. Reset goes back to the text price and plain tsc.

With a number on line 2, there's no error, and both commands write cart.js. With text, plain tsc prints the error, still writes the file, and exits with 2. --noEmitOnError prints the same error, writes nothing, and exits with 1.

Left, plain tsc: a terminal says Found 1 error, and an arrow still leads to a cart.js card. Right, tsc with --noEmitOnError: the terminal says Found 1 error, and the card says no cart.js.tscterminalFound 1 errorcart.jswritten anywaytsc --noEmitOnErrorterminalFound 1 errorno cart.jsnothing written
The same cart.ts, with one error. Plain tsc reports it and still writes cart.js. With --noEmitOnError, it reports it and writes nothing.

Many tools do only the second job. Build tools like Vite remove the types and never check them, and leave checking to your editor or to a separate tsc run. This page's Edit & run works the same way: it removes the types and runs the code, which is why the overview's example printed 34 even with its error.

Static checks and runtime checks

There are two moments you can check a value. A static check happens before the code runs: TypeScript reads your code. A runtime check happens while the code runs: your own code looks at a real value. They catch different mistakes. Here is a function that needs both:

A quantity box, checked while the code runsPop out in the code editor (opens in a new tab)TypeScript
function readQuantity(text: string) {  const quantity = Number(text);  if (Number.isNaN(quantity)) {    return 1;  }  return quantity;} const first = readQuantity("2");//    ^?console.log(first);console.log(readQuantity("two"));

Line 1 declares readQuantity, which takes the text from a quantity box. Line 2 turns it into a number with Number. Line 3 is a runtime check: Number.isNaN asks "is this NaN?". NaN means "not a number", and it's what Number gives back for text like "two". If so, line 4 gives back 1 instead. Otherwise line 6 gives back the quantity.

Line 9 calls it with "2". The query on line 10 shows const first: number: TypeScript worked out that readQuantity always gives back a number. Line 11 prints 2. Line 12 passes "two", and the runtime check makes it print 1. TypeScript couldn't have caught "two": it's text, which is exactly what the function asks for. Only the value is wrong, and values from a shopper only exist while the page runs.

The static check catches a different mistake: a wrong kind of value in your own code.

Passing a number where text goes (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function readQuantity(text: string) {  return Number(text);}console.log(readQuantity(2));

Line 4 calls readQuantity(2) with a number. TypeScript marks it on purpose: Argument of type 'number' is not assignable to parameter of type 'string'. An argument is a value you pass in a call, and a parameter is the name that receives it, like text. So: "you passed a number where the function takes text". This happens to print 2 anyway, but the next change to readQuantity might not be so forgiving.

Left, before the code runs: an editor shows readQuantity(2), and TypeScript says text is needed. Right, while the code runs: a shop page box holds the word two, and the code's own check, Number.isNaN, catches it.before it runs: staticeditorreadQuantity(2)TypeScript: a numberis not textTypeScript reads the codewhile it runs: runtimeshoptwoNumber.isNaN(…)your check catches ityour code reads the value
Before the code runs, TypeScript reads your code and catches readQuantity(2). While it runs, only your own check, Number.isNaN, can see that a shopper typed two.

The table below compares the two kinds of check. Read each row as a question, then each kind's answer.

Static checks and runtime checks
QuestionStatic check (TypeScript)Runtime check (your code)
When does it happen?Before the code runs: while you type, or when you run tsc.While the code runs, in the browser or in Node.
What does it look at?Your code: names, types, and how they're used.The real values: what a shopper typed, what a server sent.
Who writes it?Nobody. TypeScript does it from your types.You do, with code like if (Number.isNaN(quantity)).
What does it catch?A wrong kind of value in your code, a typo, a missing argument.Bad data from outside: "two" in a quantity box, a price left out.
Does it cost anything when the page runs?No. It's finished before the code runs.A little: the check is code that runs every time.
Before it runs, or while it runs?
  • const quantity: number = "2";
  • const quantity = Number("two");
  • Math.max("3", 4);
  • const data = JSON.parse("{ price: 4 }");
  • let price = 4; price.trim();
  • const prices = [3, 4]; prices[5].toFixed(2);
  • const user = { name: "Asha" }; user.nmae;
  • const total: number = JSON.parse('"7"');
Try it yourself
0 of 8 correct

Sort each line by who can catch the mistake in it. Each line is checked and run on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

A product from a server, checked both ways

A shop page asks a server for a product, and the server replies with text in a format called JSON (the JSON lesson covers JSON.parse). Real code uses both kinds of check. The static check keeps your code consistent. A runtime check guards the door where outside data comes in. Press each button to change what the server sends:

Playground: what the server sends
A product from a server replyPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number }; function toProduct(data: { name: string; price: string }): Product {  const price = Number(data.price);  if (Number.isNaN(price)) {    throw new Error(`Bad price for ${data.name}`);  }  return { name: data.name, price };} const reply = '{ "name": "Cake", "price": "4" }';const cake = toProduct(JSON.parse(reply));console.log(cake.price + 3);
When it runs7
Who catches it
TypeScriptNo errors
The check on line 5passes
Try it yourself

The server sends the price as the text "4". Line 4 turns it into the number 4, the check on line 5 passes, and the code prints 7.

TypeScript checks the code for real, and the output is what the code prints or throws. Each button changes only line 11. Reset goes back to the first reply.

Read the code from the top:

  • Line 1 says what a Product is: a name that is text and a price that is a number.
  • Line 3 declares toProduct. It takes the reply's data and promises to give back a Product. TypeScript checks that promise statically: line 8 must build an object with a text name and a number price.
  • Lines 4 to 7 are the runtime check. Line 4 turns the price into a number. If that gives NaN, line 6 throws an error, which stops the code with a message (the try, catch & finally lesson shows how to catch it).
  • Line 11 is the reply. Line 12 reads it with JSON.parse and turns it into a Product. Line 13 prints the price plus 3.

The list under the code says No errors for every reply. TypeScript can't see what a server will send. With "four" or no price at all, only the check on line 5 notices, and the code stops with Error: Bad price for Cake instead of quietly showing a wrong total. After line 12, the rest of your code can trust cake.price to be a real number, and TypeScript checks that it's used like one.

Common mix-ups

A type annotation is a promise you make to TypeScript. It doesn't change the value:

An annotation doesn't convert a valuePop out in the code editor (opens in a new tab)TypeScript
const quantity: number = JSON.parse('"2"');console.log(quantity + 1);

Line 1 reads the JSON text "2" with JSON.parse, which gives back the string "2", and says it's a number. JSON.parse can give back anything, so TypeScript believes you and reports nothing. The annotation is removed before the code runs, so quantity is still text, and line 2 prints 21.

  • "tsc runs my code." No. It reads your code and writes JavaScript. A browser or Node runs that JavaScript.
  • "An error means no JavaScript is written." No. By default, tsc writes the file anyway. Only --noEmitOnError holds it back.
  • "Writing : number turns a value into a number." No. The annotation is removed before the code runs. Use Number(…) to convert, as the 21 example shows.
  • "I can check a type with typeof while the code runs." No. typeof sees values, like 4. A name made with type, like Price, is gone by then.
  • "With TypeScript, I don't need runtime checks." No. TypeScript checks your code, not the data that arrives while it runs. A shopper's text or a server's reply still needs your own check.
  • "Every tool that handles .ts files checks the types." No. Many, like Vite and this page's Edit & run, only remove them. Checking comes from your editor or from tsc.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code to try a change: the errors update as you type, and the console shows what it prints. Guess first, then use the hints.

Exercise 1 · Warm-upCount the lines tsc writes

This is order.ts. How many lines does tsc --target es2022 write to order.js?

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Order = { item: string; quantity: number }; const order: Order = { item: "Tea", quantity: 2 };console.log(order.quantity);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict what code with an error prints

    TypeScript marks line 4. Run the code anyway: what does it print? Then fix line 4 so the error goes away.

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    function shippingCost(weight: number) {  return weight * 2;}console.log(shippingCost("3"));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAdd a runtime check to a quantity box

      A shopper typed two in the quantity box. What does the starter print? Then make readQuantity give back 1 when the text isn't a number.

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function readQuantity(text: string) {  return Number(text);}console.log(readQuantity("two"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWill tsc write the file?

        You run tsc --noEmitOnError cart.ts on this file. Does tsc write cart.js?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        const quantity: number = "2";
        console.log(quantity);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix the error without changing the output

          Make the error go away by changing only the type on line 1. What does the code print now?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          const cakePrice: number = "4";console.log(3 + cakePrice);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeGuard a server reply

            The server sent a bad price, and the starter quietly prints NaN. Add a runtime check so it prints Bad price instead. What does your fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Product = { name: string; price: number }; const reply = '{ "name": "Cake", "price": "four" }';const cake: Product = JSON.parse(reply);console.log(cake.price * 2);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, keep the moments apart: what tsc says and writes before the code runs, and what the code does when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The How TypeScript works quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What are the two jobs tsc does with a .ts file?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does typeof tea print in this code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Quantity = number;
                
                const tea: Quantity = 2;
                console.log(typeof tea);

                Choose an answer to see the explanation.

              3. Question 3 of 7tsc reports one error in cart.ts. With no other options, what happens to cart.js?

                Choose an answer to see the explanation.

              4. Question 4 of 7How many lines does tsc --target es2022 write for this file?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type User = { name: string };
                const user: User = { name: "Asha" };
                console.log(user.name);

                Choose an answer to see the explanation.

              5. Question 5 of 7This function is never called. Does TypeScript still report line 2?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function greet(name: string) {
                  return name * 2;
                }
                console.log("Hi");

                Choose an answer to see the explanation.

              6. Question 6 of 7Which of these can only be checked while the page runs?

                Choose an answer to see the explanation.

              7. Question 7 of 7TypeScript reports no errors here. What does the code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function readQuantity(text: string): number {
                  return Number(text);
                }
                console.log(readQuantity("two"));

                Choose an answer to see the explanation.

              Key takeaways

              • The TypeScript compiler, tsc, does two jobs: checking reports type errors, and emitting writes plain JavaScript.
              • Checking is static: it reads your code without running it, so it checks every line, even code that never runs.
              • Emitting removes the types: annotations, type aliases, and the rest. Everything else is copied, so the code does exactly what it did.
              • No type exists while the code runs. typeof sees values, and an annotation never converts a value.
              • By default, an error doesn't stop tsc from writing JavaScript. --noEmitOnError changes that.
              • Static checks catch mistakes in your code. Runtime checks, written by you, catch bad data that arrives while the code runs.

              Remember the one-liner.
              tsc checks your types, then removes them, and what runs is the JavaScript that's left.

              Coming next: Setting up a project. You'll install tsc, write a tsconfig.json settings file, and run TypeScript in your editor and in Node.

              CompleteFrontend Clear concepts. Working examples.