Skip to main content

🔄 JavaScript Transpilation Concepts

You want to write today's JavaScript — arrow functions, classes, optional chaining, async/await — but some of the browsers visiting your site only speak an older dialect. A transpiler is the translator that lets you author in the modern language and still ship code every browser understands. This lesson is about why that translation exists and how it works, before we touch a single config file.

Week 3 · Day 4 (Thursday: Babel and Transpilation) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what transpilation is and how it differs from compilation and minification
  • Justify why transpilation is still needed even in an evergreen-browser world
  • Trace Babel's three-phase pipeline: parse → transform → generate, and the role of the AST
  • Distinguish a syntax transform from an API polyfill and name which tool handles each
  • Predict roughly what Babel outputs for arrow functions, classes, and template literals
  • Read a target-browsers list and reason about what it will and won't down-level

Estimated Time: 55 minutes

Practice: Hand-transpile a modern snippet to ES5, then classify a mixed feature list as transform-vs-polyfill.

In This Lesson

What Is Transpilation?

Picture writing a book in modern English, then handing it to readers who only understand Shakespeare's English. You'd need a translator to rewrite your prose into a dialect they can follow — without changing what it means. That is exactly what a transpiler does for JavaScript.

A compiler translates from one language to a different, lower-level one — C to machine code, TypeScript to JavaScript. A transpiler (short for "source-to-source compiler") translates between two versions of the same language at a similar level of abstraction: modern JavaScript (ES2015 and beyond) to older JavaScript (ES5) that legacy engines accept. The tool the JavaScript world reaches for is Babel.

graph LR A[Modern JavaScript
ES2015+] --> B[Babel Transpiler] B --> C[Compatible JavaScript
ES5] C --> D[Runs in old & new browsers]

💡 Three words that get confused

  • Transpiling changes syntax to an equivalent older form (arrow function → function). Meaning is preserved.
  • Minifying shrinks code by removing whitespace and renaming variables. Behavior is preserved, readability is not.
  • Bundling stitches many modules into one file. It's about how many files, not what dialect.

A real build usually does all three, in that order-ish — but they are separate jobs.

Why We Still Need It

JavaScript ships a new edition every year, but browsers adopt those features on their own schedules, and users don't all upgrade at once. That gap creates four concrete pressures:

  • Browser support lags the spec. A feature approved this year may be unsupported in a browser someone is running today.
  • Long-lived legacy environments. Enterprise intranets, kiosks, and old mobile devices can be stuck on ancient engines for years.
  • Write tomorrow's code today. Transpilation lets your team adopt ergonomic new syntax without waiting for 100% support.
  • One consistent dialect. Developers write one modern style; the build worries about who can run what.

The JavaScript evolution timeline

The language is defined by the ECMAScript (ES) standard. The 2015 edition (ES2015, often called "ES6") was the big leap that most transpilation targets down-level to a pre-2015 baseline called ES5.

timeline title ECMAScript Editions 1997 : ES1 (first standard) 2009 : ES5 (the "safe" legacy baseline) 2015 : ES2015 / ES6 (let, const, arrow, class, modules) 2017 : ES2017 (async / await) 2020 : ES2020 (optional chaining, nullish coalescing) 2022 : ES2022 (class fields, top-level await) 2024 : ES2024 (yearly cadence continues)

⚠️ "But everyone's on evergreen browsers now!"

Largely true — and it means you can often target a much newer baseline than the old "support IE 11" default. But transpilation hasn't gone away: you still use it to ship this year's proposals before they're universal, to compile JSX and TypeScript, and to guarantee a known-good floor for the exact audience you measured. The lesson isn't "always down-level to ES5" — it's "translate exactly as far as your real users require, and no further."

How Babel Works: The Pipeline

Babel doesn't do search-and-replace on your text — that would be fragile and wrong. It works on structure. Every run flows through three phases, exactly like a careful human translator: read and understand, rewrite, then write out.

graph LR A[Source Code] --> B[Parse] B --> C[AST] C --> D[Transform
plugins & presets] D --> E[New AST] E --> F[Generate] F --> G[Output Code
+ source map]

1. Parse — understand the code

Babel reads your source and builds an Abstract Syntax Tree (AST): a tree of objects describing the code's structure ("here is a variable declaration whose value is an arrow function whose body returns..."). Parsing has two sub-steps — lexical analysis chops the text into tokens, and syntactic analysis arranges those tokens into the tree.

An arrow function's source code parsed into an abstract syntax tree of nodes VariableDeclaration ArrowFunctionExpression params: [ x ] body: x * 2
The source const double = x => x * 2; becomes a tree of typed nodes. Babel edits the tree, not the text — which is why it never mangles your strings or comments.

2. Transform — rewrite the tree

This is where the work happens. Babel walks the AST and lets plugins rewrite nodes. An arrow-function plugin, for example, spots each ArrowFunctionExpression and swaps in an equivalent FunctionExpression. A preset is just a curated bundle of plugins so you don't list them one by one.

3. Generate — write it back out

Finally Babel serializes the transformed tree back into JavaScript text and, optionally, emits a source map — a side file that maps every line of the output back to your original source so your debugger shows the code you actually wrote.

📖 Want to see it live?

Paste any snippet into the Babel REPL and watch the output change as you adjust targets. It's the fastest way to build intuition for what each feature costs.

What Babel Transforms

Let's look at the transforms you'll meet most often. In each pair, the top is what you write and the bottom is a simplified version of what an ES5 target produces.

1. Arrow functions → function expressions

// Modern JavaScript
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);

// Transpiled to ES5
var numbers = [1, 2, 3];
var doubled = numbers.map(function (n) {
  return n * 2;
});

Why it matters: arrow functions also change how this binds. Babel preserves that meaning — when an arrow captures this, the output stores it in a helper variable (often _this) so behavior stays identical.

2. Classes → constructor functions + prototypes

// Modern JavaScript
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

// Transpiled to ES5 (simplified)
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a sound.');
};

The class keyword is syntactic sugar over JavaScript's prototype system — Babel simply unwraps the sugar. (Real output adds guards like a "must call with new" check.)

3. Template literals → string concatenation

// Modern JavaScript
const name = 'World';
const greeting = `Hello, ${name}!`;

// Transpiled to ES5
var name = 'World';
var greeting = 'Hello, ' + name + '!';

4. Destructuring → indexed / property access

// Modern JavaScript
const person = { name: 'John', age: 30 };
const { name, age } = person;
const [first, second] = [1, 2, 3];

// Transpiled to ES5
var person = { name: 'John', age: 30 };
var name = person.name;
var age = person.age;

var _ref = [1, 2, 3];
var first = _ref[0];
var second = _ref[1];

💡 Notice what all four have in common

Every one of these is a syntax change — new grammar that older engines can't parse. Babel rewrites the grammar into something they can. That's a pure transform, and it's Babel's core job. The next section covers the features Babel can't fix with syntax alone.

Transforms vs Polyfills

This is the single most important distinction in this whole topic, and it trips up nearly everyone at first. There are two kinds of "new," and they need two different fixes.

graph TD A[A modern JS feature] --> B{New syntax
or new API?} B -->|New syntax| C[Transform
Babel rewrites the grammar] B -->|New API / method| D[Polyfill
core-js adds the missing function] C --> E[Examples: arrow fns, class,
template literals, destructuring] D --> F[Examples: Promise, fetch,
Array.includes, Object.assign]

Transforms fix new syntax

Arrow functions, classes, and template literals are new grammar. An old engine literally cannot parse them. Babel rewrites the grammar into equivalent old grammar. Done — no runtime code needed.

Polyfills fix new APIs

Array.prototype.includes, Promise, Object.assign, and fetch are new functions and objects, not new syntax. arr.includes(2) parses fine in ES5 — the engine just doesn't have an includes method to call. Rewriting syntax can't help; you have to add the missing implementation at runtime. That extra code is a polyfill, and Babel gets its polyfills from a library called core-js.

⚠️ Babel alone does NOT add polyfills

A common beginner bug: "I set up Babel but Promise is not defined in old browsers!" Babel transpiled your syntax perfectly — but a missing API needs core-js. You wire that up through @babel/preset-env's useBuiltIns and corejs options, which you'll configure in the next lesson.

// You write:
const arr = [1, 2, 3];
console.log(arr.includes(2));

// With useBuiltIns: 'usage' + corejs 3, Babel prepends only the
// polyfill this file actually uses:
import "core-js/modules/es.array.includes.js";
const arr = [1, 2, 3];
console.log(arr.includes(2));

✅ The one-sentence rule

Transforms change how you write things; polyfills add things that were missing. Syntax → Babel transform. Method or global object → core-js polyfill.

Targeting Browsers

Babel's smartest trick is doing only the work your audience requires. You describe your target browsers once, and @babel/preset-env figures out which transforms and polyfills are actually necessary — skipping anything the targets already support natively.

graph LR A[Your targets] --> B{Do all targets
support this feature?} B -->|Yes| C[Leave code as-is
= smaller output] B -->|No| D[Transform / polyfill it
= compatible output]

Browserslist: one query, many tools

Targets are usually declared with Browserslist syntax, shared across Babel, Autoprefixer, and other tools. You put it in package.json or a .browserslistrc file:

// package.json
{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead",
    "not op_mini all"
  ]
}

Read aloud, that says: "browsers with more than 0.5% global usage, plus the last 2 versions of each browser, excluding browsers that no longer get updates and Opera Mini." Run npx browserslist in a project to print the exact list your query resolves to today — the answer changes over time as usage stats update.

💡 Modern default vs legacy default

A contemporary app aimed at up-to-date browsers might use a tight list like "defaults" or "last 2 versions, not dead", producing lean output. Only add heavy legacy targets (like an old IE) when you have measured that audience — every legacy target you add makes the bundle bigger and slower for everyone.

Practice & Quiz

🏋️ Exercise 1: Be the transpiler

Goal: By hand, rewrite this modern snippet into equivalent ES5 (no arrow functions, no const/let, no template literals). Then paste the original into the Babel REPL (targets: ie 11) and compare.

const greet = (name) => {
  return `Hi, ${name}!`;
};
console.log(greet('Ada'));
💡 Hint

Turn const into var, the arrow into a function expression, and the template literal into string concatenation with +.

✅ Solution
var greet = function (name) {
  return 'Hi, ' + name + '!';
};
console.log(greet('Ada'));

This is a pure syntax transform — no runtime helper or polyfill is needed, because nothing new was called, only new grammar was used.

🏋️ Exercise 2: Transform or polyfill?

Goal: For each feature, decide whether an old browser needs a Babel syntax transform or a core-js polyfill. Write your answer before revealing the solution.

FeatureYour call
a ?? b (nullish coalescing)?
Promise.resolve()?
[1,2].flat()?
class Dog {}?
obj?.prop (optional chaining)?
✅ Solution
  • a ?? btransform (new operator = new syntax)
  • Promise.resolve()polyfill (missing global object)
  • [1,2].flat()polyfill (missing array method)
  • class Dog {}transform (new syntax)
  • obj?.proptransform (new syntax)

Rule of thumb: if it's an operator or keyword, it's syntax → transform. If it's a method call or a named global, it's an API → polyfill.

🎯 Quick Quiz

Question 1: What best describes what a transpiler does?

Question 2: Old browser, [1,2,3].includes(2) throws "includes is not a function." What fixes it?

Question 3: In which phase does Babel actually rewrite your code?

Best Practices & Pitfalls

✅ Do

  • Let @babel/preset-env + a Browserslist query decide the work — don't hand-pick transforms
  • Keep the difference clear in your head: syntax → transform, API → polyfill
  • Ship source maps so you debug the code you wrote, not the transpiled output
  • Target the narrowest browser set your real users need — smaller output, faster loads

❌ Don't

  • Assume Babel adds polyfills automatically — you must enable core-js
  • Reflexively target ancient browsers "just in case" — it bloats every bundle
  • Confuse transpiling with minifying or bundling — they are separate build steps
  • Transpile your node_modules — it's slow and usually unnecessary

⚠️ The regeneratorRuntime is not defined classic

When old targets force async/await to be transformed, the output depends on a runtime helper. If you see this error, you're missing the polyfill/runtime wiring — the fix (via useBuiltIns or @babel/plugin-transform-runtime) is covered in the next lesson.

Summary

🎉 Key Takeaways

  • Transpilation translates modern JS into equivalent older JS while preserving meaning
  • Babel runs three phases: parse → transform → generate, editing an AST, not raw text
  • Transforms fix new syntax; polyfills (from core-js) fix missing APIs
  • Babel does not add polyfills on its own — you enable them via @babel/preset-env
  • A Browserslist target tells the toolchain to do only the work your real audience needs

📚 Additional Resources

🚀 What's Next?

Now that you understand what transpilation is and why the transform-vs-polyfill split matters, the next lesson gets hands-on: Configuring Babel — choosing a config file, wiring up @babel/preset-env with useBuiltIns and corejs, and tailoring builds per environment.

🎉 Concept unlocked!

You can now explain, from memory, why the same "modern feature" sometimes needs a rewrite and sometimes needs a whole new function shipped alongside it. That mental model is the foundation for every config choice ahead.