cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Type checking vs type stripping

See why Vite, esbuild, and Node run TypeScript without checking it, what type stripping leaves behind, and how tsc --noEmit catches errors in builds and CI.

By the end, you can
  • 01
    Tell the two jobs apartExplain why stripping types is fast and file by file, and why only checking finds mistakes between files.
  • 02
    Run TypeScript in NodeWrite code Node can strip: .ts import paths, type-only imports, and no syntax that makes new JavaScript.
  • 03
    Check before you shipAdd tsc --noEmit to a build script and a CI workflow, so a type error stops the build.

Two jobs: checking and stripping

A shop changed its price formatting. formatPrice now needs a currency as well as a price:

export function formatPrice(price: number, currency: string) {  return `${currency}${price.toFixed(2)}`;}

Line 1 says formatPrice takes a price and a currency. Line 2 puts the currency in front of the price with two decimals, so formatPrice(7, "$") is $7.00. But one file still calls it the old way, and TypeScript marks it, on purpose here:

cart.ts, still calling the old way (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
import { formatPrice } from "./format.ts"; console.log(formatPrice(7));

Line 1 imports formatPrice from format.ts. Line 3 calls it with only a price. TypeScript says Expected 2 arguments, but got 1. To see that, it had to read both files: the call is in cart.ts, and the rule is in format.ts. Now run the project two ways:

Run it with Node, then check it with tscText
$ node cart.ts
undefined7.00
$ npx tsc --noEmit
cart.ts(3,13): error TS2554: Expected 2 arguments, but got 1.

Lines 1 and 2: Node runs cart.ts and prints undefined7.00, with no warning. The missing currency is undefined, and the template literal writes it as text. Lines 3 and 4: npx tsc --noEmit prints the error and writes no files. cart.ts(3,13) means "in cart.ts, line 3, column 13". This is tsc's plain form, the one it prints into a log. In your own terminal it adds colors and shows the line of code under the message.

These are two different jobs. Type checking reads your whole project and reports every type that doesn't fit. Type stripping removes the types from one file so it can run, and reports nothing. Node strips. So do Vite, esbuild, and the other fast tools from the last lesson. Only tsc, and your editor, check.

Left, checking with tsc --noEmit: cart.ts and format.ts both go into a terminal that shows error TS2554 and exit code 2. Right, stripping with Vite or Node: cart.ts becomes cart.js, which runs and prints undefined7.00.Checking: tsc --noEmitcart.tsformat.tsterminalerror TS2554exit code 2reads every file togetherfinds the mistakeStripping: Vite and Nodecart.tscart.jsone file, types removedundefined7.00runs anyway
Two jobs. Checking reads every file together, so it sees that cart.ts calls formatPrice wrongly, and writes nothing. Stripping turns one file into JavaScript and runs it, mistake and all.
Real-life analogyA print shop and a proofreader

A print shop prints whatever pages you send, quickly, one at a time. It doesn't read them. A proofreader is slower, but reads the whole book and notices when chapter 9 says something chapter 2 doesn't allow. A careful publisher uses both: fast printing while drafting, and a proofreader before the book goes out.

In real life: The print shop prints each page as it comes, fast
In TypeScript: Vite and Node strip each file and run it
In real life: It prints a typo just as neatly as a correct word
In TypeScript: Stripping runs code with type errors
In real life: The proofreader reads the whole book and sees chapter 9 contradict chapter 2
In TypeScript: tsc reads every file and sees a call that doesn't match its function
In real life: The publisher won't ship until the proofreader signs off
In TypeScript: CI runs tsc --noEmit before anything ships
The main idea

Checking and stripping are separate jobs. Stripping removes types from one file so it can run: fast, and blind to type errors. Checking reads every file and reports errors: only tsc and your editor do it. Most projects strip to run the code, and run tsc --noEmit to check it.

What stripping does to your code

To strip a file, a tool finds every piece of TypeScript syntax (annotations, type lines, interface blocks) and removes it. Nothing else changes. Tools differ in how they remove it. Compare Node and tsc on the same file:

Playground: two ways to remove types
cart.ts, what you writePop 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));
return is online 4
What Node runs for cart.tsPop 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));
Try it yourself

Node replaces every type with spaces of the same length. Line 1, the type line, is now blank, and : CartItem on line 3 is a gap. Nothing moves, so return is still on line 4, at the same column. An error in this code points at the right place in cart.ts, with no extra file.

Node's own stripTypeScriptTypes and TypeScript 5.9's ts.transpileModule made these outputs in this lesson's tests; the page shows the saved results. Both print 6. Reset goes back to Node.

In cart.ts, line 1 is a type, line 3 declares lineTotal with three annotations, line 4 multiplies, and line 8 prints 6. With Node, every type became spaces of the same length: line 1 is blank, and item: CartItem became item and ten spaces. With tsc, the types are gone and the code is laid out again, so the return line moved from line 4 to line 2. Both print 6.

Three versions of the lineTotal line. In cart.ts it is line 3 with item: CartItem and quantity: number. Node's version is also line 3, with spaces where the types were. tsc's version is line 1, with the types removed and the code moved.cart.ts, line 3lineTotal(item: CartItem, quantity: number)Node, line 3lineTotal(item          , quantity        )same line, same columnstsc, line 1lineTotal(item, quantity)moved: a source map points back
Node turns each type into spaces, so quantity stays on line 3 at the same column. tsc removes the types and the lines around them, so the same code moves to line 1, and a source map has to point back.

Why does the position matter? When code throws an error, the message says which line and column it came from. With Node's blank spaces, that is the right place in your .ts file. With output that moved, tools write a source map: a .map file that links each position in the output back to your code, so your browser's developer tools can show cart.ts, line 4 (the Source maps in depth lesson shows how). tsc writes one when tsconfig.json has "sourceMap": true; Vite writes them for you while you work.

Stripping is fast because it never asks what a type means. It only finds where types are. That is also why it can't find a missing argument: knowing that formatPrice needs two arguments means reading format.ts, and understanding its types.

Bundlers strip each file and never check

Vite's own documentation says it plainly: Vite only removes types from .ts files and does not check them (see Vite's TypeScript notes). Since Vite 8, released in March 2026, it removes them with Oxc, a fast compiler written in Rust; Vite 7 and earlier did the same job with esbuild. Its dev server turns each file into JavaScript the moment the browser asks for it, one file at a time, so a change shows up almost instantly. Checking would mean reading every file again on every change. So the dev server serves code with type errors:

Left: an editor shows formatPrice(7) with the note Expected 2 arguments. An arrow labelled Vite strips leads right to a browser window that shows undefined7.00.editorformatPrice(7)Expected 2 argumentsthe editor checksVite stripsbrowserundefined7.00the page loads anyway
Your editor checks as you type and marks formatPrice(7). Vite only strips the types, so the page loads and shows undefined7.00. The two never talk to each other.

While you work, your editor is the checker: it marks formatPrice(7) as you type. If you want every file checked as you save, run npx tsc --noEmit --watch in a second terminal. The --watch option keeps tsc running and checks again after each change. (A Vite plugin called vite-plugin-checker can also show the errors in the browser.)

vite build, which makes the files you put online, doesn't check either. The Vite starter's build script runs tsc first and joins the two with &&, which runs the second command only if the first one succeeds (its exit code, the number a command ends with, is 0). Try both scripts on both versions of cart.ts:

Playground: does the build stop?
cart.ts with the mistakePop out in the code editor (opens in a new tab)TypeScript
import { formatPrice } from "./format.ts"; console.log(formatPrice(7));
npm run buildvite build
vite buildwrites dist/
The built page printsundefined7.00
Try it yourself

vite build only strips types, so it builds dist/ from the code with the mistake. The page the shopper gets prints undefined7.00. Nothing in the build warned you.

The tsc output and exit codes are what tsc 5.9 prints for these files. Pick a script, then a version of cart.ts. Reset goes back to vite build and the mistake.

With vite build alone, both versions build, and the broken page prints undefined7.00. With tsc --noEmit && vite build, tsc exits with code 2 for the broken version, and the build never starts. For the fixed version, tsc exits with 0, and Vite builds a page that prints $7.00.

Not every tool skips the check. The table below shows what common commands do with the same type error. Read each row as: the command, whether it checks types, and what happens.

What each command does with a type error
The commandDoes it check types?With a type error
vite, the dev serverNo.It serves the page anyway. Your editor shows the error.
vite buildNo.It builds dist/ anyway.
next buildYes, by default.It stops the build and prints the error.
node cart.tsNo.It runs the code anyway.
npx tsx cart.tsNo.It runs the code anyway.
npx tsc --noEmitYes, every file in the project.It prints the error and exits with code 2.

next build is the exception: Next.js runs a type check as part of its production build and stops on errors, unless its typescript.ignoreBuildErrors setting turns that off. tsx, a popular package for running .ts files in Node, strips with esbuild and never checks.

Node's built-in type stripping

Since Node 22.18 and 23.6, node cart.ts runs a TypeScript file directly (the Setting up a project lesson showed the first run). Node replaces types with spaces, as the playground above showed, and never checks them. To stay that simple, it follows a few rules:

  • It ignores tsconfig.json. No target, no paths, no settings at all.
  • Imports name the real file, ending included: "./types.ts", not "./types".
  • A type import says type. Node strips one file at a time, so it can't know that a name from another file is only a type.
  • No syntax that makes new JavaScript, like enum.
  • It won't strip .ts files inside node_modules, and it doesn't run .tsx files.

The third rule is the easiest to miss. checkout.ts imports a type and a function from types.ts, which exports CartItem and formatPrice. Pick how line 1 imports them:

Playground: import a type for Node
checkout.tsTypeScript
import { CartItem, formatPrice } from "./types.ts"; const tea: CartItem = { name: "Tea", price: 3 };console.log(formatPrice(tea.price));
Commandnode checkout.ts
What Node printsText
SyntaxError: The requested module './types.ts' does not provide an export named 'CartItem'
TypeScript1 error: line 1
Try it yourself

Node strips checkout.ts without reading types.ts. It sees a normal import of two names, so it asks types.ts for both, and CartItem isn't there: types are stripped from that file too. TypeScript, with verbatimModuleSyntax on, warns you first: 'CartItem' is a type and must be imported using a type-only import.

The real compiler checks checkout.ts next to types.ts with module nodenext and verbatimModuleSyntax. What Node prints is what node checkout.ts printed with Node 22. The playground only checks; Reset goes back to the first import.

With a plain import, Node stops before running anything: does not provide an export named 'CartItem'. Types are stripped from types.ts too, so there is no CartItem to import. With type in front of the name, or import type on its own line, Node removes it and prints $3.00. The setting verbatimModuleSyntax makes TypeScript point at the plain import before Node does: 'CartItem' is a type and must be imported using a type-only import.

Node's documentation recommends these settings for code it strips. Each one makes TypeScript warn you about something Node would get wrong:

tsconfig.json for code Node runs directlyJSON
{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}
  • Line 3, noEmit: Node runs the .ts files, so tsc only checks.
  • Line 4, target: esnext, because Node never rewrites syntax, and line 5, module: nodenext, so imports are checked Node's way.
  • Line 6, rewriteRelativeImportExtensions, allows imports that end in .ts, and turns them into .js if tsc ever does write JavaScript.
  • Line 7, erasableSyntaxOnly, reports syntax that can't be stripped. Line 8, verbatimModuleSyntax, reports type imports without type.

Here is erasableSyntaxOnly at work on an enum, with the error on purpose:

A size enum with erasableSyntaxOnly on (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
enum Size {  Small = "small",  Large = "large",}console.log(Size.Large);

Lines 1 to 4 make an enum of sizes, and line 5 prints large when tsc compiles it (the Enums lesson explains why it needs real code). TypeScript marks line 1: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. Node stops at the same line:

What node prints for the enumText
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

The table below lists the four kinds of syntax that Node's documentation says it can't strip. Read each row as: the code, what Node says, and what to write instead.

Syntax Node can't strip
When you writeNode saysWrite instead
enum Size { Small, Large }TypeScript enum is not supported in strip-only modeA const object with as const, or a union of strings.
namespace Shop { export const name = "Tea"; }TypeScript namespace declaration is not supported in strip-only modeA module: put the code in its own file and export it.
class Cart { constructor(private items: string[]) {} }TypeScript parameter property is not supported in strip-only modeDeclare the field in the class body and set it in the constructor.
import fs = require("fs");TypeScript import equals declaration is not supported in strip-only modeimport fs from "node:fs";, an ordinary import.

Bundlers like Vite turn an enum into JavaScript themselves, so it works there; the limit is Node's. If you need the whole language in Node, npx tsx cart.ts runs any TypeScript, still without checking. An older package, ts-node, does check types by default, which makes it slower; its --transpileOnly option turns that off.

Checking in CI

Your editor checks the files you have open, and a teammate might forget to run npm run build. To make sure nothing unchecked ships, teams use CI, short for continuous integration: a service that runs your commands on a fresh computer every time someone pushes code. GitHub's is called GitHub Actions. Its instructions live in a workflow file in your repository:

.github/workflows/check.ymlYAML
name: Check
on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - run: npm ci
      - run: npx tsc --noEmit
      - run: npm run build
  • Line 1 names the workflow. Line 2 runs it on every push and every pull request.
  • Line 6 asks for a fresh Linux computer. Lines 8 to 11 copy your code onto it and install Node 22.
  • Line 12, npm ci, installs exactly the packages in package-lock.json.
  • Line 13 checks every file. Line 14 builds the page, but only if every step before it passed.

CI knows a step failed from its exit code. tsc exits with 0 when it finds no errors, and with 2 when it finds any. Push both versions of cart.ts:

Playground: a CI run for each push
The runfailed
npm ci: passedInstalls the packages in package-lock.json.
npx tsc --noEmit: failedcart.ts(3,13): error TS2554: Expected 2 arguments, but got 1. Error: Process completed with exit code 2.
npm run build: skippedSkipped: an earlier step failed.
Try it yourself

Every push starts the workflow on a fresh computer. npm ci installs the packages. npx tsc --noEmit finds the missing argument and exits with code 2, so the step fails, the build step is skipped, and the run is marked failed. The pull request shows a red cross, and the broken code doesn't ship.

The type-check log line is tsc's real output for this cart.ts; the last line is how GitHub Actions reports a failing step. Reset goes back to the first push.

The log line is the same plain form you saw in the overview: file, then line and column in brackets, then the error number and message. tsc prints this form whenever its output goes to a log instead of a terminal, so CI logs stay easy to read and search.

A git push card points to a CI window with three steps: npm ci, npx tsc --noEmit, and npm run build. From the check step, exit code 2 leads to: run fails, nothing ships. Exit code 0 leads to: build, then ship.git pushCInpm cinpx tsc --noEmitnpm run buildexit code 2run fails, nothing shipsexit code 0build runs, then ships
Every push runs the same steps on a fresh computer. If npx tsc --noEmit exits with code 2, the run fails and the build never starts. Only code with no type errors reaches the build.

Two options make checking faster on your own computer. --watch keeps tsc running and rechecks on every save. --incremental saves what tsc learned in a file called tsconfig.tsbuildinfo, so the next run only rechecks what changed.

A checkout project that checks before it ships

Here are the scripts of a Vite checkout project that uses everything in this lesson:

package.json scripts for a checkout projectJSON
{
  "scripts": {
    "dev": "vite",
    "check": "tsc --noEmit",
    "check:watch": "tsc --noEmit --watch",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview"
  }
}
  • Line 3, npm run dev: Vite's dev server, fast, never checks. Your editor checks the files you have open.
  • Line 4, npm run check: checks every file once. This is the command CI runs.
  • Line 5, npm run check:watch: rechecks every file each time you save, in a second terminal.
  • Line 6, npm run build: checks, and builds only if the check passed.
  • Line 7 serves the built page so you can look at it.

Every command in this lesson either stops on a type error or carries on. Sort them.

Does the mistake stop it?
  • npm run dev (Vite) with a missing argument
  • vite build with a missing argument
  • node cart.ts with a missing argument
  • npx tsx cart.ts with a missing argument
  • node sizes.ts with an enum in it
  • node checkout.ts with import { CartItem, formatPrice }, where CartItem is a type
  • npx tsc --noEmit with a missing argument
  • tsc --noEmit && vite build with a missing argument
Try it yourself
0 of 8 correct

Sort each command by what it does with the mistake described.

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

Common mix-ups

  • "Vite checks my types when it builds." No. vite build only strips them. Put tsc --noEmit && in front of it.
  • "The page loads in the dev server, so the code has no type errors." No. The dev server serves code with type errors. Look at your editor, or run npm run check.
  • "node cart.ts uses my tsconfig.json." No. Node ignores it completely: no paths, no target.
  • "Stripping changes what my code does." No. It only removes types. Stripped code does exactly what the same code without types would do.
  • "--noEmit means tsc can't fail." No. It writes no files, but it still exits with code 2 when it finds an error, and CI fails the step.
  • "tsx and ts-node are the same." No. tsx strips without checking. ts-node checks types by default, unless you pass --transpileOnly.

The table puts the two jobs side by side. Read each row as a question, then each job's answer.

Checking and stripping, side by side
QuestionCheckingStripping
How many files does it read?Every file in the project, together.One file at a time.
Does it report type errors?Yes. That's its whole job.No. It only removes types.
Does it read tsconfig.json?Yes: strict, lib, module, and the rest.Node doesn't. Vite reads a few settings, like verbatimModuleSyntax.
How fast is it?Slower: it has to understand every type.Fast: it only finds and removes types.
Who does it?tsc, and your editor for the files you have open.Vite, esbuild, SWC, Babel, tsx, and Node.

Practice exercises

Some exercises are TypeScript code, checked the way the exercise says; others are a log, a script, or a workflow to read or fix. Try a guess first, then use the hints.

Exercise 1 · Warm-upPredict what Node prints

format.ts is the one from the overview: formatPrice(price, currency). TypeScript marks line 3. What does node cart.ts print? Then fix the call.

Starter codePop out in the code editor (opens in a new tab)TypeScript
import { formatPrice } from "./format.ts"; console.log(formatPrice(4));

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

    Exercise 2 · Warm-upRead a CI log

    A CI run failed with this log. Which line of checkout.ts do you open?

    Starter codeText
    Run npx tsc --noEmit
    checkout.ts(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
    Error: Process completed with exit code 2.

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

      Exercise 3 · PracticeImport a type so Node can strip it

      This project runs node checkout.ts and checks with verbatimModuleSyntax. TypeScript marks line 1, and Node would stop there. Fix the import. Which word do you add?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      import { CartItem, formatPrice } from "./types.ts"; const cake: CartItem = { name: "Cake", price: 4 };console.log(formatPrice(cake.price));

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

        Exercise 4 · PracticeMake the build stop on type errors

        This project's npm run build shipped a page with a type error. Change the build script so it can't happen again. What should the script be?

        Starter codeJSON
        {
          "scripts": {
            "dev": "vite",
            "build": "vite build"
          }
        }

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

          Exercise 5 · ChallengeMake an enum strippable

          Node can't strip this enum, and erasableSyntaxOnly marks it. Rewrite it so Node can run it and Size still works as a type. What does your code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          enum Size {  Small = "small",  Large = "large",}console.log(Size.Large);

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

            Exercise 6 · ChallengeAdd the missing CI step

            This workflow builds the page but never checks types. Add a step. Which command does it run?

            Starter codeYAML
            jobs:
              check:
                runs-on: ubuntu-latest
                steps:
                  - uses: actions/checkout@v7
                  - uses: actions/setup-node@v7
                  - run: npm ci
                  - run: npm run build

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

              Check your understanding

              For each question, ask which tool is running, and whether it checks types or only strips them. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The checking vs stripping quiz · 7 questionsScore: first tries count
              1. Question 1 of 7cart.ts calls a function from format.ts with one argument too few. Which command reports it?

                Choose an answer to see the explanation.

              2. Question 2 of 7You run this file with node cart.ts. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                // format.ts exports formatPrice(price: number, currency: string)
                import { formatPrice } from "./format.ts";
                
                console.log(formatPrice(5));

                Choose an answer to see the explanation.

              3. Question 3 of 7What happens when Node runs this file?

                Read the code, then predictTypeScript
                enum Status {
                  Paid,
                  Pending,
                }
                console.log(Status.Paid);

                Choose an answer to see the explanation.

              4. Question 4 of 7What does Node put where : number was?

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does Node need type in import { type CartItem, formatPrice } from "./types.ts"?

                Choose an answer to see the explanation.

              6. Question 6 of 7In CI, npx tsc --noEmit finds one error. What happens to the run?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which build stops on a type error without any extra script?

                Choose an answer to see the explanation.

              Key takeaways

              • Stripping removes types from one file so it can run. It's fast, and it never reports a type error.
              • Checking reads every file together. Only tsc and your editor do it; tsc --noEmit checks without writing files.
              • Vite's dev server and vite build only strip. Put tsc --noEmit && in front of the build.
              • Node replaces types with spaces, ignores tsconfig.json, and needs .ts import paths and import type.
              • Node can't strip enum, namespaces with code, parameter properties, or import =; erasableSyntaxOnly flags them.
              • CI runs tsc --noEmit on every push. Its exit code, 2 on any error, fails the run.

              Remember the one-liner.
              Tools strip types to run your code fast; tsc checks it, and CI makes sure that check passes before anything ships.

              Coming next: Types in JavaScript files. You'll check plain .js files with JSDoc comments and checkJs, a first step towards TypeScript.

              CompleteFrontend Clear concepts. Working examples.