
Transpiler: Babel, TypeScript
Translator for old browsers. Converting ES6+ to ES5. How is it different from a Compiler?

Translator for old browsers. Converting ES6+ to ES5. How is it different from a Compiler?
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

The monitoring alert went off at 2 AM. "Users can't log in!" I frantically opened Sentry to find Promise is not defined errors flooding in from IE11 users only. I had set up Babel, I wrote ES6 code - why was it breaking on IE11?
The answer was simple. Babel transforms syntax, but it doesn't automatically add new objects or methods (Promise, Array.includes, etc.). That's what polyfills do, and I hadn't configured core-js properly.
After this incident, I had to truly understand what transpilers do and don't do, how they differ from compilers, and how Babel's preset and plugin system actually works. This is what I learned.
At first, they seemed similar. Both take code as input and transform it into something else. But while building frontend build pipelines, I grasped the critical difference.
Compilers change the abstraction level of the language. C code to assembly, Java to bytecode. High-level language to low-level language. Like adapting a novel into a movie script - the medium itself changes.
Transpilers transform languages at similar abstraction levels. TypeScript to JavaScript, ES2023 JavaScript to ES5 JavaScript. Changing dialects within the same medium. Like translating Korean to North Korean dialect, or British English to American English.
The official term is "Source-to-Source Compiler." It's still a form of compilation, but the purpose and output level are different - that's what clicked for me.
When ES6 (ES2015) came out in 2015, frontend developers rejoiced. const, let, arrow functions, classes, Promises, destructuring... finally, we could write code with modern syntax.
But reality was harsh. 30% of our customers still used IE11. IE11 barely supported ES6. We couldn't abandon modern syntax though. Developer experience (DX) and code quality demanded it.
The solution to this dilemma was Babel. I write code in ES6+, and Babel transforms it to ES5 at build time. Developers code in the future, users execute in browsers from the past. Time travel architecture.
To understand exactly how Babel transforms code, I dug into its internals. The core concept is AST (Abstract Syntax Tree) transformation.
For example, transforming an arrow function to a regular function:
// Input code
const add = (a, b) => a + b;
// 1. Parse - Generate AST
{
type: "ArrowFunctionExpression",
params: [{ name: "a" }, { name: "b" }],
body: {
type: "BinaryExpression",
operator: "+",
left: { name: "a" },
right: { name: "b" }
}
}
// 2. Transform - ArrowFunctionExpression -> FunctionExpression
{
type: "FunctionExpression",
params: [{ name: "a" }, { name: "b" }],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "+",
left: { name: "a" },
right: { name: "b" }
}
}]
}
}
// 3. Generate - Code generation
var add = function(a, b) {
return a + b;
};
Understanding this AST transformation process made Babel's plugin system click. Each plugin finds and transforms specific AST nodes.
When I first configured Babel, the difference between presets and plugins was most confusing. I eventually understood it like this.
Plugin: A single syntax transformation feature. For example, @babel/plugin-transform-arrow-functions only transforms arrow functions.
Preset: A bundle of multiple plugins. @babel/preset-env includes dozens of plugins.
Adding necessary plugins one by one is inefficient. That's why we use presets in production.
// .babelrc
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["> 0.5%", "last 2 versions", "not dead", "IE 11"]
},
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
Let me break down what this configuration means.
targets: Defines which browsers to support. Market share above 0.5%, last 2 versions of each browser, and IE11.
useBuiltIns: "usage": This is the polyfill key. It only adds polyfills for modern features actually used in your code. Setting it to "entry" adds all polyfills needed for target browsers, bloating bundle size.
corejs: 3: Polyfill library version. Means we're using core-js@3.
Babel isn't the only tool that needs browser targets. Autoprefixer (CSS vendor prefixes), ESLint, Webpack - many tools need this information. Configuring each separately creates inconsistencies.
That's why we use browserslist for unified configuration.
// package.json
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"IE 11"
]
}
Or create a .browserslistrc file:
# .browserslistrc
> 0.5%
last 2 versions
not dead
IE 11
Now Babel, Autoprefixer, all tools share this configuration. One setting unifies the entire build pipeline's targets.
Back to the 2 AM incident. Why couldn't Babel transform Promise?
Babel only transforms syntax. Arrow functions (=>) to regular functions, const to var. But it can't transform new objects or methods (Promise, Map, Set, Array.includes, etc.). These need to exist at runtime.
That's where polyfills come in. Polyfills implement missing features in old browsers using JavaScript.
// Promise polyfill example (how core-js adds it)
if (typeof Promise === 'undefined') {
window.Promise = function(executor) {
// Promise implementation code...
};
Promise.prototype.then = function(onFulfilled, onRejected) {
// then implementation...
};
// ...
}
core-js is a library providing polyfills for all ES5+ features. Used with Babel's useBuiltIns: "usage", it automatically imports only the features you actually use.
// Code I wrote
const promise = Promise.resolve(42);
[1, 2, 3].includes(2);
// Code transformed by Babel + core-js
import "core-js/modules/es.promise.js";
import "core-js/modules/es.array.includes.js";
var promise = Promise.resolve(42);
[1, 2, 3].includes(2);
I didn't add polyfills, but Babel automatically imported what's needed. That's the magic of useBuiltIns: "usage".
The TypeScript compiler (tsc) is also a transpiler. But its role differs from Babel.
// Input: TypeScript code (ES2020 + types)
interface User {
name: string;
age: number;
}
const getUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const user: User = await getUser(1);
console.log(user?.name);
// Output: JavaScript code (ES5, depending on tsconfig target)
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
// async/await polyfill code...
};
var getUser = function (id) {
return __awaiter(this, void 0, void 0, function () {
var response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4, fetch("/api/users/" + id)];
case 1:
response = _a.sent();
return [2, response.json()];
}
});
});
};
var user = await getUser(1);
console.log(user === null || user === void 0 ? void 0 : user.name);
What tsc did:
interface User, : number, : Promise<User>, etc.)async/await to generator-based polyfill (downleveling)?.) to ternary operatorsThis is Type Stripping (removing types) and Downleveling (transforming to lower level).
TypeScript compiler behavior is controlled by tsconfig.json.
{
"compilerOptions": {
"target": "ES5", // Output JavaScript version
"module": "commonjs", // Module system (commonjs, es6, esnext, etc.)
"lib": ["ES2020", "DOM"], // Available built-in API type definitions
"strict": true, // Strict type checking
"esModuleInterop": true, // CommonJS/ES Module compatibility
"skipLibCheck": true, // Skip type checking in node_modules
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true, // Babel/SWC compatibility (independent file transformation)
"noEmit": true // Don't generate .js files (type checking only)
}
}
target is key. Setting it to ES5 transforms arrow functions, const, classes, etc. to ES5 syntax. ES2020 leaves them as-is.
noEmit: true is interesting. Modern projects use TypeScript only for type checking, leaving actual transpilation to Babel or SWC. The reason is speed.
In production, TypeScript projects often use Babel together. Why?
Using TypeScript only:tsc@babel/preset-typescript)tsc --noEmit)I concluded:
Small projects: Just use tsc
Large projects: Babel for transpiling, TypeScript for type checking
The common weakness of Babel and tsc is speed. They're written in JavaScript and slow on large projects.
Enter SWC (Rust-based) and esbuild (Go-based) to solve this.
SWC (Speedy Web Compiler):Speed comparison on the same project:
# Same project build time
Babel: 15s
tsc: 12s
SWC: 0.8s
esbuild: 0.3s
However, SWC and esbuild don't have Babel's rich plugin ecosystem. If you need heavy custom transformations, stick with Babel. But for most common cases, SWC or esbuild is sufficient - that's what I realized.
Transpilers aren't JavaScript-only. The CSS world has transpilers too.
Sass/SCSS: SCSS is a CSS superset. Provides variables, nesting, mixins, functions, etc. Browsers don't understand SCSS, so the Sass compiler transforms it to CSS.
// Input: SCSS
$primary-color: #3498db;
$padding: 16px;
.button {
background-color: $primary-color;
padding: $padding;
&:hover {
background-color: darken($primary-color, 10%);
}
&--large {
padding: $padding * 2;
}
}
/* Output: CSS */
.button {
background-color: #3498db;
padding: 16px;
}
.button:hover {
background-color: #2980b9;
}
.button--large {
padding: 32px;
}
PostCSS: PostCSS transforms CSS with JavaScript plugins. Think of it as Babel for CSS.
The most famous plugin is Autoprefixer. Write modern CSS, and it automatically adds vendor prefixes.
/* Input */
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
/* After Autoprefixer */
.container {
display: -ms-grid;
display: grid;
-ms-grid-columns: (1fr)[3];
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
PostCSS is also Tailwind CSS's core engine. It transforms Tailwind's utility classes into actual CSS.
In real projects, transpilers aren't used standalone. They integrate with bundlers like Webpack, Vite, Rollup.
Webpack + Babel example:// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react'
]
}
}
}
]
}
};
The flow:
import statements to find files.js, .jsx, .ts, .tsx files, passes them to babel-loaderWith Vite: Development mode uses esbuild for fast TypeScript transformation, production build uses Rollup + Babel (or SWC).
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
esbuild: {
target: 'es2015'
},
build: {
target: 'es2015'
}
});
Here's the final configuration that solved my 2 AM incident.
// package.json
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead",
"IE 11"
],
"dependencies": {
"core-js": "^3.30.0"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.21.0",
"babel-loader": "^9.1.0"
}
}
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3,
"modules": false,
"debug": true // Log which polyfills are added
}
]
]
}
Running the build showed this in the console:
Using polyfills with `usage` option:
[/path/to/file.js] Added following core-js polyfills:
es.promise
es.array.includes
es.object.assign
es.string.starts-with
Now I could verify that polyfills were added for every modern feature I used. Testing on IE11 again - it worked perfectly.
I settled on this framework:
Need legacy browser support → Babel + core-js.babelrc when neededUnderstanding transpilers revealed the essence of frontend development. We write code with future syntax, and transpilers make it work on browsers from the past.
The difference between compilers and transpilers is clear. Compilers change language levels (high-level → low-level), transpilers change dialects at the same level (ES2023 → ES5, TypeScript → JavaScript).
Babel's AST transformation, preset and plugin system, the difference between polyfills and syntax transformation, TypeScript compiler's dual role (type checking + transpiling), the speed revolution of SWC and esbuild, and even CSS transpilers.
All these tools aim for one goal: Let developers write code with the best tools, and let users execute it in any environment.
The 2 AM incident was painful, but it became an opportunity to deeply understand how transpilers work. Now I'm confident configuring polyfills, browserslist, and build pipelines.
Transpilers aren't just tools. They're the time machines of the frontend ecosystem.