JavaScript transpilation is the process of converting source code written in one version or flavour of JavaScript into equivalent JavaScript that a different environment can run. A transpiler (also called a source-to-source compiler, or trans-compiler) reads modern syntax such as optional chaining, class private fields, JSX or TypeScript, and outputs code with the same behaviour that older browsers, or any browser in the case of JSX and TypeScript, understand. Babel, the TypeScript compiler, esbuild and SWC are the most common JavaScript transpilers.
This guide explains how transpilation works, how it differs from compilation and polyfilling, and which tool to use in 2026. Every example below was run with current versions (Babel 8.0, TypeScript 7.0, esbuild 0.28, SWC 1.16 and Node.js 24), and the outputs shown are real.
Transpilation vs Compilation
| Compilation | Transpilation | |
|---|---|---|
| Input → output | High-level language → lower-level code (machine code, bytecode, WebAssembly) | Source code → source code at a similar level of abstraction |
| Example | C → machine code; Java → JVM bytecode | TypeScript → JavaScript; ES2022 → ES2017; JSX → jsx() calls |
| Output readable by people? | Usually not | Yes, it’s still JavaScript |
Transpilation is really a kind of compilation, and the tools are often called compilers (the TypeScript command is tsc, “TypeScript compiler”). The distinction people care about is that a transpiler’s output is the same kind of language as its input.
Why JavaScript Needs Transpiling
- New syntax, old browsers. JavaScript gets new features every year (ES2015, ES2020, ES2022…). If you support browsers that predate a feature, the new syntax is a syntax error there, and the whole script fails.
- Languages browsers don’t run. Browsers don’t understand JSX or TypeScript at all, so every React or TypeScript project is transpiled before it ships.
- Upcoming features. You can use proposals before every browser supports them.
How a Transpiler Works

- Parse: the source text is turned into an abstract syntax tree (AST), a tree of nodes such as “optional member expression” or “class private property” (parsing itself is explained in CSS parsing and HTML parsing; JavaScript works the same way).
- Transform: plugins walk the tree and replace nodes your targets don’t support with equivalent older constructs.
- Generate: the modified tree is printed back out as JavaScript, usually with a source map so DevTools can show you the original code.
What Transpiled Code Looks Like
Here is a small module using three modern features:
class Cart {
#items = []; // private field (ES2022)
add(item) { this.#items.push(item); return this; }
get total() { return this.#items.reduce((sum, i) => sum + i.price, 0); }
}
const user = { profile: null };
const city = user.profile?.address?.city ?? 'unknown'; // optional chaining + nullish coalescing (ES2020)
const last = [1, 2, 3].at(-1); // Array.prototype.at (ES2022): an API, not syntax
export { Cart, city, last };Babel 8 with a target of Chrome 70 (a 2018 browser) turned it into this (helper functions shortened):
var _user$profile$address, _user$profile;
import "core-js/modules/es.array.at.js"; // polyfill import, see below
var _items = /*#__PURE__*/new WeakMap();
class Cart {
constructor() {
_classPrivateFieldInitSpec(this, _items, []);
}
add(item) {
_classPrivateFieldGet(_items, this).push(item);
return this;
}
...
}
const user = { profile: null };
const city = (_user$profile$address = (_user$profile = user.profile) === null || _user$profile === void 0 || (_user$profile = _user$profile.address) === null || _user$profile === void 0 ? void 0 : _user$profile.city) !== null && _user$profile$address !== void 0 ? _user$profile$address : 'unknown';
const last = [1, 2, 3].at(-1);The private field became a WeakMap and optional chaining became a chain of null checks. esbuild and SWC produced the same structure with their own helper names, for example esbuild:
const city = (_c = (_b = (_a = user.profile) == null ? void 0 : _a.address) == null ? void 0 : _b.city) != null ? _c : "unknown";And JSX, which no browser understands, becomes ordinary function calls:
// src/Button.jsx
export function Button({ label }) {
return <button className="primary">{label}</button>;
}
// esbuild --jsx=automatic output
import { jsx } from "react/jsx-runtime";
export function Button({ label }) {
return /* @__PURE__ */ jsx("button", { className: "primary", children: label });
}Transpiling Is Not Polyfilling
Notice that [1, 2, 3].at(-1) came through unchanged. Transpilers rewrite syntax; they can’t create a built-in method that the browser doesn’t have. Array.prototype.at, Object.groupBy, String.prototype.replaceAll and the new Set methods are APIs, and in an old browser they’d throw “is not a function” at runtime. A polyfill adds them by defining the missing method in JavaScript. The standard library of polyfills is core-js.
Babel can add the right core-js imports automatically, but the setup changed in Babel 8: preset-env‘s old useBuiltIns option was removed (Babel 8 throws an error if you use it), and the job moved to babel-plugin-polyfill-corejs3:
{
"targets": "chrome 70",
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }]
],
"plugins": [
["polyfill-corejs3", { "method": "usage-global", "version": "3.51" }]
]
}One trap I hit while testing: when targets was set inside the preset-env options instead of at the top level, the polyfill plugin didn’t see it. It polyfilled Object.groupBy and the new Set methods but silently skipped .at() and .replaceAll(), which Chrome 70 also lacks. With targets at the top level, as above, it added all of them:
import "core-js/modules/es.array.at.js";
import "core-js/modules/es.object.group-by.js";
import "core-js/modules/es.set.union.v2.js";
import "core-js/modules/es.string.replace-all.js";
...Whatever tool you use, test the final bundle in the oldest browser you support.
TypeScript: Type Stripping and tsc
interface User { name: string; age?: number }
export function greet(user: User): string {
return `Hello, ${user.name}`;
}
console.log(greet({ name: 'Ada' }));Compiling TypeScript mostly means deleting the type annotations. tsc from TypeScript 7 (the new natively compiled version) produced:
export function greet(user) {
return `Hello, ${user.name}`;
}
console.log(greet({ name: 'Ada' }));Two practical notes from testing TypeScript 7: it now requires rootDir in tsconfig.json when it can’t infer the layout (my first build failed with error TS5011 until I added it), and type checking and transpiling are separate jobs. Bundlers strip types quickly with a fast transpiler (Vite 8 uses Oxc, Next.js uses SWC) and leave checking to tsc --noEmit in your editor and CI.
Node.js runs TypeScript files directly
$ node src/greet.ts # Node.js 24
Hello, Ada
$ node src/enum.ts # file containing a TypeScript enum
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
$ node --experimental-transform-types src/enum.ts
1Node.js now strips TypeScript types itself: Node 24 ran the file with no warning, while Node 23 still printed an “experimental” warning. It only removes types, so syntax that needs real code generation (enums, namespaces, parameter properties) needs --experimental-transform-types or a transpiler. TypeScript’s erasableSyntaxOnly option warns you about such syntax.
Choosing a Transpiler in 2026
I transpiled the same 1.18 MB JavaScript file (React DOM’s development build) with each tool, three times each, from the command line:
| Tool | Written in | Time (3 runs, incl. process start) | Use it when |
|---|---|---|---|
| esbuild 0.28 | Go | 195–232 ms | You want very fast standalone builds; used by many bundlers and tools |
| SWC 1.16 | Rust | 278–321 ms | You use Next.js or another SWC-based tool, or want a fast Rust transpiler with plugins |
TypeScript 7 tsc | Go (native) | 344–393 ms | You need type checking or TypeScript-specific output such as declaration files |
| Babel 8.0 | JavaScript | 599–721 ms | You need custom or experimental syntax plugins, or precise core-js polyfilling |
Each run includes starting the process through npx (roughly 150 ms), so the gaps are larger inside a long-running bundler. In practice you rarely call a transpiler directly: frameworks run one for you (Vite 8 uses the Rust-based Oxc through Rolldown, which I didn’t benchmark here; Next.js uses SWC), and you control it through a targets setting (often a browserslist query such as "defaults" or "chrome 70"). The more modern your targets, the less code is rewritten, and the smaller and faster your bundle is (see how to speed up JavaScript).
Benefits and Drawbacks
| Benefits | Drawbacks |
|---|---|
| Write modern, readable code and still support older browsers | Extra build step and configuration |
| Use JSX and TypeScript | Transpiled code is larger and can be slower than native syntax |
| Adopt new language features early | Debugging needs source maps |
| One codebase for many environments | Doesn’t add missing APIs: you still need polyfills |
Conclusion
Transpilation turns modern JavaScript, JSX or TypeScript into JavaScript your target environments can run, by parsing code into a syntax tree, rewriting unsupported parts and printing it back out. It fixes syntax, not missing APIs, so pair it with polyfills when you support old browsers, and keep your targets as modern as your audience allows. For most projects the transpiler is built into the toolchain (Oxc in Vite 8, SWC in Next.js, esbuild in many others), with Babel for special cases and tsc for type checking.
FAQs
What is transpilation in JavaScript?
Converting source code into equivalent source code for a different target, for example turning ES2022, JSX or TypeScript into JavaScript that older browsers or any browser can run. Tools that do this are called transpilers, such as Babel, esbuild, SWC and the TypeScript compiler.
What is the difference between transpiling and compiling?
Compiling usually turns a high-level language into lower-level code such as machine code or bytecode. Transpiling turns source code into other source code at a similar level, such as TypeScript into JavaScript. Transpiling is a kind of compiling.
What is the difference between transpiling and polyfilling?
Transpiling rewrites syntax, such as optional chaining or class fields. Polyfilling adds missing built-in APIs at runtime, such as Array.prototype.at or Object.groupBy. Supporting old browsers usually needs both.
Is TypeScript compiled or transpiled?
Both terms are used. The TypeScript compiler (tsc) type-checks the code and transpiles it to JavaScript, mostly by removing type annotations. Tools like esbuild, SWC and Node.js can strip the types without checking them.
Is Babel still needed?
Less often. Modern toolchains use faster transpilers written in Go or Rust, such as Oxc (Vite 8), SWC (Next.js) and esbuild. Babel is still useful for custom syntax plugins and fine-grained core-js polyfilling, and Babel 8 was released for those cases.
Can Node.js run TypeScript without a transpiler?
Yes, for most files. Node.js 24 strips type annotations natively. Syntax that needs code generation, such as enums and namespaces, requires the –experimental-transform-types flag or a transpiler.








