cf.completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

How new syntax is downleveled

See what compilers turn new JavaScript into: classes, private fields, async functions, loose vs exact output, polyfills, and what can't be downleveled.

By the end, you can
  • 01
    Read compiled outputRecognize what a class, a private field, an async function and ?. turn into for older targets.
  • 02
    Choose exact or looseExplain when loose output gives a different answer, and which compiler options decide it.
  • 03
    Know the limitsTell what needs a transform, what needs a polyfill, and what can't be added to an old engine at all.

Downleveling: the same meaning in older syntax

Downleveling means compiling code to an older version of JavaScript. The compiler rewrites new syntax into older syntax that does the same thing, so older engines can run it. TypeScript uses the word, and Babel and SWC do the same job.

The transpilers and polyfills lesson explains targets and tools. This lesson opens up the output, so you can read what your code becomes. Every compiled example here is real output from TypeScript 5.9.

A class with a private fieldPop out in the code editor (opens in a new tab)JavaScript
class Cart {
  #count = 0;
  add() {
    this.#count += 1;
    return this.#count;
  }
}
const cart = new Cart();
cart.add();
console.log(cart.add());

Line 2 gives each cart a private #count, which starts at 0. Line 4 adds 1 to it. Line 9 calls add once, and line 10 calls it again and prints 2.

The same class compiled for ES2015Pop out in the code editor (opens in a new tab)JavaScript
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Cart_count;
class Cart {
    constructor() {
        _Cart_count.set(this, 0);
    }
    add() {
        __classPrivateFieldSet(this, _Cart_count, __classPrivateFieldGet(this, _Cart_count, "f") + 1, "f");
        return __classPrivateFieldGet(this, _Cart_count, "f");
    }
}
_Cart_count = new WeakMap();
const cart = new Cart();
cart.add();
console.log(cart.add());

ES2015 has classes but no private fields, so the compiler keeps the class and rewrites #count. Lines 1 to 11 are two helper functions that read and write a private value. Line 22 makes a WeakMap, and line 15 stores 0 in it, with the cart as the key.

Lines 18 and 19 do what lines 4 and 5 did, through the helpers. The output is 25 lines instead of 10, and it still prints 2.

A card class with #count points through a function card labeled compiler to a card class with WeakMap. Under both outer cards: prints 2.class with #countprints 2compilertarget: ES2015class + WeakMapprints 2
The compiler rewrites the private field #count into code an ES2015 engine understands: a WeakMap and two helper functions. Both versions print 2.
Real-life analogyRewriting a recipe for an older kitchen

Picture a recipe written for an air fryer, and a friend whose kitchen only has an oven. You rewrite the steps for the oven: more steps, longer times, the same dish. But if the recipe needs a baking tray your friend doesn't have, rewriting the steps won't help. They have to buy the tray.

In real life: A recipe that uses an air fryer
In JavaScript: Code with new syntax, such as #count
In real life: The same recipe rewritten for an oven
In JavaScript: The downleveled output
In real life: The same dish at the end
In JavaScript: The same printed result
In real life: "Buy a baking tray first"
In JavaScript: A polyfill for a missing built-in
The main idea

Downleveling rewrites new syntax into older syntax that means the same thing. The output is longer, and only as exact as the options you choose. It can't add missing built-ins: those need polyfills.

Classes and private fields

Classes arrived in ES2015. Before that, the same job was done with a constructor function and its prototype. So for an ES5 target, every class becomes a function.

A small classPop out in the code editor (opens in a new tab)JavaScript
class Cart {
  constructor(price) {
    this.price = price;
  }
  total(tax) {
    return this.price + tax;
  }
}
console.log(new Cart(3).total(1));
The same class compiled for ES5Pop out in the code editor (opens in a new tab)JavaScript
var Cart = /** @class */ (function () {
    function Cart(price) {
        this.price = price;
    }
    Cart.prototype.total = function (tax) {
        return this.price + tax;
    };
    return Cart;
}());
console.log(new Cart(3).total(1));

Line 2 of the output is the constructor, now a plain function named Cart. Line 5 puts total on Cart.prototype, where every cart can find it. Lines 1 and 9 wrap it all in a function that runs at once and gives back Cart. Both versions print 4.

The output is close, but not exact. A real class throws a TypeError if you call it without new; this function doesn't. Babel adds a small check for that by default. The class basics lesson covers what classes do.

Private fields are harder. The value must be stored somewhere other code can't reach, and it must go away when the object does. A WeakMap does both. Here is the idea, written by hand.

A private count kept in a WeakMapPop out in the code editor (opens in a new tab)JavaScript
const counts = new WeakMap();
class Cart {
  constructor() {
    counts.set(this, 0);
  }
  add() {
    counts.set(this, counts.get(this) + 1);
    return counts.get(this);
  }
}
const cart = new Cart();
cart.add();
console.log(cart.add(), Object.keys(cart).length);

Line 1 makes the WeakMap. Line 4 stores 0 for each new cart, with the cart as the key. Line 7 reads the count, adds 1, and stores it again. Line 13 prints 2 0: the count is 2, and the cart has no own properties at all.

Step through a private field kept in a WeakMap
Step 0 of 6Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The WeakMap model runs for real.

Running in
  1. script
Next: line 1
class Cart {  constructor() {    counts.set(this, 0);  }  add() {    counts.set(this, counts.get(this) + 1);    return counts.get(this);  }}const cart = new Cart();cart.add();console.log(cart.add(), Object.keys(cart).length);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step to watch the count change beside line 4, while the cart itself stays empty. This replays the example code; it isn't a view inside the JavaScript engine.

On the left, a cart labeled cart with nothing inside, labeled no own keys. On the right, a box labeled counts holding a tag reading cart and 2. An arrow from the tag points back at the cart.cartno own keyscart→ 2countsthe key
The count isn't stored on the cart. The WeakMap on line 1 keeps it, with the cart as the key. That is why line 13 prints 2 0: the count is 2, and the cart has 0 own keys.

The compiler's helpers add one more thing. Line 3 of the ES2015 output checks that the WeakMap has this object, and throws a TypeError if not. That matches a real private field, which throws when read from an object of another class. The private members lesson covers that rule.

Async functions and generators

Async functions arrived in ES2017, and generators in ES2015. A generator can pause at yield and continue later, which is what an async function does at await. So for an ES2015 target, an async function becomes a generator.

An async functionPop out in the code editor (opens in a new tab)JavaScript
async function brew() {
  const tea = await Promise.resolve("tea");
  return tea + " ready";
}
brew().then((text) => console.log(text));

Line 2 waits for a promise that gives "tea". Line 3 returns "tea ready", and line 5 prints it.

The same function compiled for ES2015Pop out in the code editor (opens in a new tab)JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function brew() {
    return __awaiter(this, void 0, void 0, function* () {
        const tea = yield Promise.resolve("tea");
        return tea + " ready";
    });
}
brew().then((text) => console.log(text));

Line 11 hands a generator function, written function*, to a helper called __awaiter. Line 12 shows the await turned into yield.

Lines 1 to 9 are the helper. It starts the generator on line 7. Each time the generator yields a promise, line 6 waits for it, and line 4 resumes the generator with the result. When the generator returns, the helper's own promise resolves. It still prints tea ready.

ES5 has no generators either. So for ES5, the body becomes a state machine: a switch over numbered steps, with a helper called __generator that remembers which step comes next. The transpiled async lesson walks through one step by step. Each extra rewrite makes the output bigger.

Read each row of the table as: "for this target, the async example becomes this many lines and characters, with these helpers."

The async example, compiled for each target
TargetLinesCharactersHelpers added
ES5503045__awaiter, __generator
ES201516889__awaiter
ES20195140none
ES20225140none

From ES2017 on, the code is left as written. For ES5 it is more than 20 times longer than the original, and it calls Promise, which ES5 engines don't have. That one needs a polyfill too.

Loose output and spec-compliant output

Spec-compliant output behaves exactly like the real feature, even in rare cases. Loose output is smaller and faster, because it assumes your code avoids those rare cases. When the assumption is wrong, you get a different answer.

Spreading a SetPop out in the code editor (opens in a new tab)JavaScript
const flavors = new Set(["mint", "mint", "lemon"]);
console.log([...flavors].length);

Line 1 makes a Set, which keeps each value once, so it holds mint and lemon. Line 2 spreads it into an array and prints 2.

Compiled for ES5, the default wayPop out in the code editor (opens in a new tab)JavaScript
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
var flavors = new Set(["mint", "mint", "lemon"]);
console.log(__spreadArray([], flavors, true).length);

Line 11 calls the __spreadArray helper. Line 8 copies with Array.prototype.slice, which reads length and numbered indexes. A Set has neither, so the copy is empty, and it prints 0. TypeScript reports error TS2802 for this when it checks your types, but the compiler still writes the output.

Compiled for ES5 with downlevelIterationPop out in the code editor (opens in a new tab)JavaScript
var __read = (this && this.__read) || function (o, n) {
    var m = typeof Symbol === "function" && o[Symbol.iterator];
    if (!m) return o;
    var i = m.call(o), r, ar = [], e;
    try {
        while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
    }
    catch (error) { e = { error: error }; }
    finally {
        try {
            if (r && !r.done && (m = i["return"])) m.call(i);
        }
        finally { if (e) throw e.error; }
    }
    return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
var flavors = new Set(["mint", "mint", "lemon"]);
console.log(__spreadArray([], __read(flavors), false).length);

With the downlevelIteration option, line 27 first passes the Set through __read. Line 2 finds the Set's iterator, and line 6 asks it for each value. This is how a real spread works, so it prints 2. The cost is 16 more lines.

Two rows starting from a card Set of mint and lemon. Top row, loose ES5 output: slice gives an empty array, so the length is 0. Bottom row, with downlevelIteration: the iterator gives mint and lemon, so the length is 2.Set(2)sliceiterator[] → 0[mint, lemon] → 2loose: wrongspec-compliant: right
The default ES5 output copies with slice, which only works on array-like values, so a Set gives an empty array and it prints 0. With downlevelIteration, the output walks the Set's iterator and prints 2, like the original.

Class fields have the same kind of choice. A class field is defined on the object; it is not assigned.

A class field and a parent's setterPop out in the code editor (opens in a new tab)JavaScript
class Base {
  set price(value) {
    console.log("setter ran with " + value);
  }
}
class Cart extends Base {
  price = 3;
}
const cart = new Cart();
console.log(Object.hasOwn(cart, "price"));

Line 2 gives Base a setter for price. Line 7 is a class field with the same name. The field is defined straight on the cart, so the setter never runs, and line 10 prints true: price is the cart's own property.

Compiled for ES2020, assigning the fieldPop out in the code editor (opens in a new tab)JavaScript
class Base {
    set price(value) {
        console.log("setter ran with " + value);
    }
}
class Cart extends Base {
    constructor() {
        super(...arguments);
        this.price = 3;
    }
}
const cart = new Cart();
console.log(Object.hasOwn(cart, "price"));

With useDefineForClassFields turned off, line 9 assigns the field with =. Assigning finds the setter on Base and calls it, so it prints setter ran with 3, then false.

Compiled for ES2020, defining the fieldPop out in the code editor (opens in a new tab)JavaScript
class Base {
    set price(value) {
        console.log("setter ran with " + value);
    }
}
class Cart extends Base {
    constructor() {
        super(...arguments);
        Object.defineProperty(this, "price", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 3
        });
    }
}
const cart = new Cart();
console.log(Object.hasOwn(cart, "price"));

With the option on, lines 9 to 14 use Object.defineProperty, which skips setters, so it prints true like the original. TypeScript turns the option on by default when the target is ES2022 or later.

Babel calls these choices assumptions. Since Babel 7.13 you can list them in your config, and each one makes the output smaller by trusting your code. Read each row of the table as: "this assumption trusts this, and if it's wrong, this happens."

Four Babel assumptions, and what goes wrong if they're false
AssumptionWhat it trustsWhat goes wrong if it's false
iterableIsArrayEvery value you spread or loop over is an array.Spreading a Set gives the wrong result.
setPublicClassFieldsA class field never has the same name as a setter.The parent's setter runs when the field is set.
noClassCallsClasses are always called with new.Calling a class without new doesn't throw.
privateFieldsAsPropertiesSoft privacy is enough for private fields.Private values become hidden properties on the object.

Transforms and polyfills

A compiler changes syntax: the way code is written. It doesn't add built-ins: functions and objects that come with the engine. Here is one line with both kinds.

New syntax and a new method togetherPop out in the code editor (opens in a new tab)JavaScript
const order = { prices: [3, 4, 5] };
console.log(order.prices?.at(-1) ?? 0);

Line 2 reads order.prices safely with ?., takes the last price with .at(-1), and falls back to 0 with ??. It prints 5.

Compiled for ES2019Pop out in the code editor (opens in a new tab)JavaScript
var _a, _b;
const order = { prices: [3, 4, 5] };
console.log((_b = (_a = order.prices) === null || _a === void 0 ? void 0 : _a.at(-1)) !== null && _b !== void 0 ? _b : 0);

?. and ?? arrived in ES2020, so they are rewritten. Line 1 makes two temporary variables. Line 3 checks _a against null and undefined where ?. was, and does the same for _b where ?? was.

But .at(-1) is still there. It is a method that arrived in ES2022, and to a compiler it's an ordinary method call. In an engine without it, this line throws a TypeError. The fix is a polyfill: code that adds the missing method before your code runs.

Two halves. On the left, the syntax ?. and ?? goes through a compiler card and becomes older syntax. On the right, the method .at() is left as it is, and needs a polyfill card that adds it.new syntax?. and ??compilerrewrites the codenew method.at(-1)polyfilladds the method
In the ES2019 output, the compiler rewrote ?. and ?? into older checks, but .at(-1) stays as it is. A compiler changes syntax; a missing method needs a polyfill.

TypeScript never adds polyfills. Babel can: with @babel/preset-env and useBuiltIns: "usage", it adds imports from the core-js library for the built-ins each file uses. Some features need both a transform and a polyfill. Private fields need WeakMap, and async functions need Promise.

What does the compiler turn it into?
  • #count = 0, compiled for ES2015
  • #count in cart, compiled for ES2015
  • async function brew(), compiled for ES2015
  • async () => 1, compiled for ES2015
  • async function brew(), compiled for ES5
  • function* steps(), compiled for ES5
  • prices.at(-1), compiled for ES5
  • Object.groupBy(items, fn), compiled for ES5
Try it yourself
0 of 8 correct

Put each piece of code with what the compiler makes of it, for the target given.

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

What can't be downleveled

Some features can't be rewritten or added, because they need the engine itself to behave differently. Proxy is the best-known one.

A proxy that gives 0 for missing pricesPop out in the code editor (opens in a new tab)JavaScript
const prices = new Proxy({}, {
  get(target, name) {
    return name in target ? target[name] : 0;
  },
});
prices.tea = 3;
console.log(prices.tea, prices.cake);

Lines 2 to 4 are a get trap. It runs every time a property is read, and gives 0 for names the object doesn't have. Line 7 prints 3 0: tea was set on line 6, but cake was never set.

ES5 can run code when a property is read, but only with a getter, and only for names listed ahead of time. Nothing runs when an unknown name such as cake is read. The proxy-polyfill library from Google only handles properties that exist when the proxy is made, so it can't run this example. The Proxy lesson shows what traps can do.

Read each row of the table as: "this feature can't be added, for this reason, so do this instead."

Features no transform or polyfill can fully add
FeatureWhy it can't be addedWhat to do instead
ProxyIts traps run on every action, even reading a name that doesn't exist yet. Older engines have no hook for that.Use getter methods, or require browsers that have Proxy.
WeakRef and FinalizationRegistryThey depend on the garbage collector, which code can't control.Use a Map and remove entries yourself.
Lookbehind, such as (?<=\$)\d+Only the regular expression engine can look backwards.Use a capture group, such as \$(\d+), and read the group.
BigInt operators, such as 2n ** 64nCode can't teach + or ** a new kind of number.Use a library such as JSBI, which uses function calls.

For features like these, check for the feature before using it, and give older browsers a simpler path, or none at all.

Choosing a target

The target decides how much gets rewritten. Pick a feature and a target to see the real output, how big it is, and what it prints when it runs in your browser.

Playground: one feature, four targets
Before compilingJavaScript
class Cart {
  #count = 0;
  add() {
    this.#count += 1;
    return this.#count;
  }
}
const cart = new Cart();
cart.add();
console.log(cart.add());
Compiled for ES2015JavaScript
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Cart_count;
class Cart {
    constructor() {
        _Cart_count.set(this, 0);
    }
    add() {
        __classPrivateFieldSet(this, _Cart_count, __classPrivateFieldGet(this, _Cart_count, "f") + 1, "f");
        return __classPrivateFieldGet(this, _Cart_count, "f");
    }
}
_Cart_count = new WeakMap();
const cart = new Cart();
cart.add();
console.log(cart.add());
Run in this browserrunning…
Output for ES2015TypeScript 5.9
lines25
characters1450
helpers__classPrivateFieldGet, __classPrivateFieldSet
printsrunning…
Try it yourself
A private field, from ES2022, compiled for ES2015.

#count became a WeakMap, read and written by two helper functions. The class stays a class.

TypeScript 5.9 made every output with ts.transpileModule, in this lesson's tests. The page shows the saved results, then runs the one you pick for real in this browser.

Try ES2022 first: all three are left as written. Then step down to ES5 and watch the output grow, while the printed answer stays the same.

Some rules of thumb for real projects:

  • Set the target to the oldest browsers you really support, not lower. Every step down makes the code bigger and slower for every user.
  • Share the helpers. With importHelpers, TypeScript imports them from the tslib package instead of copying them into every file. Babel does the same with @babel/plugin-transform-runtime.
  • Keep the exact options unless you have measured a problem: downlevelIteration for ES5, and useDefineForClassFields left on.
  • Run your tests on the compiled code, not just the source. That is where loose output shows its differences.

Common mix-ups

Most mix-ups come from thinking the compiled code is always an exact copy of the original.

  • "Compiled code always behaves the same." Only spec-compliant output aims for that. Loose output, and options such as useDefineForClassFields turned off, can change the answer.
  • "TypeScript adds the polyfills an old target needs." No. It only rewrites syntax. Add polyfills yourself, or let Babel add them with core-js.
  • "A compiled private field is a normal property." No. By default it lives in a WeakMap, so Object.keys can't see it.
  • "ES5 is the safe target." It makes the code bigger and slower for everyone. Current versions of all major browsers run ES2022.
  • "Anything can be polyfilled." No. Proxy, WeakRef, lookbehind and BigInt operators need the engine itself.

The table below puts three of these side by side. Read each row as: "people often think this, but this is true."

What is true about compiled code
TopicWhat people thinkWhat is true
Loose outputIt behaves exactly like the original.It assumes your code avoids rare cases, such as spreading a Set.
TypeScriptIt adds the polyfills an old target needs.It only rewrites syntax. You add polyfills, or let Babel add them with core-js.
Private fieldsThey become normal properties.By default they live in a WeakMap, not on the object.

Practice exercises

Each exercise looks at one rewrite from this lesson. Guess first, then run it.

Exercise 1 · Warm-upPredict ?? next to ||

What does line 2 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const stock = 0;
console.log(stock ?? 5, stock || 5);

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

    Exercise 2 · Warm-upPredict a value kept in a WeakMap

    What does line 11 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const secrets = new WeakMap();
    class Cup {
      constructor(size) {
        secrets.set(this, size);
      }
      get size() {
        return secrets.get(this);
      }
    }
    const cup = new Cup(3);
    console.log(cup.size, cup.secret, Object.keys(cup).length);

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

      Exercise 3 · PracticeSort syntax from built-ins

      What does line 3 print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const features = ["??", "Array.prototype.at", "class fields", "Object.groupBy", "?."];
      const kinds = features.map((name) => (/^[A-Z]/.test(name) ? "polyfill" : "transform"));
      console.log(kinds.join(" "));

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

        Exercise 4 · PracticeFix the loose spread

        This prints 0. Change line 5 so it counts the Set's values, as [...flavors] would. What does your program print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        var __spreadArray = function (to, from) {
          return to.concat(Array.prototype.slice.call(from));
        };
        const flavors = new Set(["mint", "lemon"]);
        console.log(__spreadArray([], flavors).length);

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

          Exercise 5 · ChallengeDefine the field instead of assigning it

          This prints setter 3, then undefined. Change line 9 so it defines size on the cup, the way a class field does. What does your program print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Base {
            set size(value) {
              console.log("setter " + value);
            }
          }
          class Cup extends Base {
            constructor() {
              super();
              this.size = 3;
            }
          }
          console.log(new Cup().size);

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

            Exercise 6 · ChallengeWrite a generator as a state machine

            Rewrite steps without function*: return an object with a next method. What does your program print?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            function* steps() {
              yield "boil";
              yield "pour";
            }
            const it = steps();
            console.log(it.next().value, it.next().value, it.next().done);

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

              Check your understanding

              For each question, ask whether the feature is syntax or a built-in, and whether the output is exact or loose. Each answer explains why it's right or wrong.

              The downleveling quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does TypeScript turn a private field into for an ES2015 target?

                Choose an answer to see the explanation.

              2. Question 2 of 6What does this loose spread helper print, next to a real spread?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                var __spreadArray = function (to, from) {
                  return to.concat(Array.prototype.slice.call(from));
                };
                console.log(__spreadArray([], new Set([3, 4])).length, [...new Set([3, 4])].length);

                Choose an answer to see the explanation.

              3. Question 3 of 6Does this class field run the parent's setter?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                class Base {
                  set size(value) {
                    console.log("setter");
                  }
                }
                class Cup extends Base {
                  size = 3;
                }
                console.log(Object.hasOwn(new Cup(), "size"));

                Choose an answer to see the explanation.

              4. Question 4 of 6What does an ES2015 target do with an async function?

                Choose an answer to see the explanation.

              5. Question 5 of 6Which of these needs a polyfill rather than a compiler transform?

                Choose an answer to see the explanation.

              6. Question 6 of 6Why can't Proxy be added to an ES5 engine?

                Choose an answer to see the explanation.

              Key takeaways

              Compiled code is your code in older words. Knowing what it turns into helps you pick the right target and options.

              • Downleveling rewrites new syntax into older syntax that means the same thing.
              • Classes become functions for ES5. Private fields become a WeakMap before ES2022.
              • Async functions become generators for ES2015, and state machines for ES5.
              • Loose output is smaller but can give a different answer, such as spreading a Set.
              • Compilers change syntax; polyfills add built-ins. Some features need both.
              • Proxy, WeakRef, lookbehind and BigInt operators can't be added to an old engine.

              Remember the one-liner.
              A compiler can rewrite how you say it, but it can't give an engine abilities it doesn't have.

              Coming next: Working with ASTs. It shows how tools like these read your code as a tree, walk it, and change it.

              CompleteFrontend Clear concepts. Working examples.