Why this matters

Browsers do not run TypeScript. They run JavaScript. Every TypeScript file you write goes through a compile step that turns it into JavaScript before it can ship to a user. Understanding that step explains a lot: why TypeScript can use features the browser hasn't shipped, why type errors at build time still let you ship code, and why your tsconfig.json has a target setting.

The shorthand: TypeScript is what you write. JavaScript is what the browser runs.

TypeScript is a build-time language. The runtime sees JavaScript, every time.- the practical rule

What the compiler strips

The bulk of what the compiler does is delete things. Types do not exist at runtime, so everything type-shaped is removed:

  • Type annotations on variables, parameters, return types - all gone.
  • interface and type declarations - gone.
  • Generic type parameters - gone (the values still flow at runtime, just without the types attached).
  • as casts and satisfies assertions - gone.
  • Import statements that only imported types - removed entirely.
// What you write (example.ts)
type User = { id: number; name: string };

function greet(user: User): string {
  return `Hello, ${user.name}!`;
}

// What the browser runs (example.js)
function greet(user) {
  return `Hello, ${user.name}!`;
}

The type User declaration vanishes. The parameter annotation vanishes. The return-type annotation vanishes. What's left is identical to JavaScript you could have written yourself.

What it keeps

Everything that has runtime meaning stays:

  • Variable declarations, function bodies, control flow - your actual program.
  • class declarations - they become JavaScript classes.
  • enum declarations - these compile to real runtime objects (one of the few TypeScript features that add code rather than remove it; many teams avoid enums for this reason and use string-literal unions instead).
  • Decorators - emitted as runtime calls if you opt in.
  • JSX/TSX - transformed into React.createElement calls (or the equivalent for your framework).

If a feature has any effect on what your program does at runtime, the compiler keeps it. If it only describes shape to the type-checker, it's removed.

Downleveling and targets

Beyond stripping types, the compiler can downlevel - rewrite modern JavaScript syntax into an older form for browsers that don't support the new syntax. The target setting in tsconfig.json controls this:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",   // emit ES2022 syntax
    "module": "ESNext",   // keep modern imports
    "strict": true
  }
}

If you set target: "ES5", your async/await becomes a state machine, your arrow functions become function expressions, your let/const becomes var. The behaviour is preserved; the syntax goes back in time. In 2026 most projects target ES2020 or later because their browser-support floor has moved on.

The benefit is that you can write modern code today and not worry about which browsers parse it. The cost is that downleveled output is sometimes larger and slower than the original. Reach for downleveling only when you actually need it.

Two jobs, often split

A common confusion: "compiling TypeScript" feels like one job. It is actually two:

  • Type-checking. Read the source, build a type graph, verify everything lines up, report errors. This is slow - on a big codebase, several seconds. It is also the part that catches your bugs.
  • Type-stripping (transpiling). Read the source, remove the type annotations, emit JavaScript. This is fast - milliseconds even on huge codebases.

Modern toolchains split these two jobs. Vite, for example, uses esbuild for type-stripping (so the dev server starts in milliseconds and HMR is instant) and lets the TypeScript compiler do type-checking in a separate worker (or in your editor). The trade-off:

  • esbuild / swc / Babel - strip types fast, don't catch type errors.
  • tsc --noEmit - type-check thoroughly, don't produce output.
  • Your editor + CI - run tsc --noEmit in the background; the dev server keeps using fast strippers.

This is why a type error in your code does not always prevent the dev server from running. The dev server's stripper does not care about types; only the type-checker does. You can ship code that tsc rejects - the browser will run it - but most teams configure CI to block merges on tsc errors. Don't make a habit of it.

tsc, esbuild, Babel, swc

The compilers and transpilers you'll meet:

  • tsc - the official TypeScript compiler from Microsoft. Does both jobs - type-checks and emits JavaScript. The reference implementation; slowest but most thorough.
  • esbuild - Go-based. Strips types extremely fast (no type-checking). What Vite uses for dev.
  • swc - Rust-based. Same role as esbuild; used by Next.js, Deno, and others. Strips types fast.
  • Babel - the older transpiler, still used in legacy toolchains. Strips TypeScript via a plugin. Less common in new projects.

The practical setup in 2026: write TypeScript, develop with Vite (esbuild stripping), let your editor and CI run tsc --noEmit for type checking, build for production with Vite (which hands off to Rolldown). The browser sees JavaScript.

The takeaway

  • The browser only ever runs JavaScript. TypeScript is a build-time language.
  • Compilation strips types and downlevels syntax - that's it. The runtime semantics of your code don't change.
  • Type-checking and type-stripping are separate jobs, often done by separate tools. That split is why dev servers stay fast on huge codebases.
  • A type error doesn't stop the dev server - it's caught by the type-checker, not the stripper. Configure CI to block on type errors.

For the language explainer, read What is TypeScript? For the runtime model underneath the emitted JavaScript, read The event loop in JavaScript. For the toolchain that orchestrates the compile step in modern projects, read What is Vite?