A8 · Part 3 · Advanced
Ecosystem & professional
How the tools around JS actually work, and where the language itself comes from.
05On this page
TypeScript and Node each have their own shelf on this site, reserved for exactly this kind of depth once they're written — this section stays brief on both, just enough to place them correctly next to plain JS. Testing gets a full chapter of its own, right after this one, because unlike the other two it's squarely inside what "advanced JavaScript" actually means day to day.
TypeScript and Node, briefly
TypeScript adds a type system checked entirely at compile time and
erased before anything runs — it's structural, meaning two
differently-named types with the same shape are compatible, unlike
languages that check by declared name. Generics, narrowing, and the
built-in utility types (Partial, Pick,
Omit) are where that gets interesting — reserved for
the TypeScript shelf once it's written.
Node extends JS past the browser with a filesystem, a process, and
no DOM — streams, cluster (multi-process scaling across
CPU cores), worker threads, and AsyncLocalStorage
(request-scoped context that survives across
awaits without threading a parameter through every
function) are the parts worth a real chapter — reserved the same way
for the Node shelf.
The next chapter covers testing in full: unit vs integration vs E2E, mocking, fake timers, and what coverage does and doesn't actually tell you.
Beyond Node: Deno and Bun
| Node | Deno | Bun | |
|---|---|---|---|
| Module system | CommonJS + ESM, needs config to choose | ESM by default, URL imports work directly | ESM + CommonJS, both work with no config |
| TypeScript | needs a transpile step | runs .ts files directly, no build step | runs .ts files directly, no build step |
| Security default | full access, no sandboxing | no file/network/env access unless explicitly granted with a flag | full access, same as Node |
| Built-in tooling | needs separate packages for testing, bundling, linting | test runner, formatter, linter, and bundler all built in | test runner, bundler, and a much faster package manager all built in |
Both were built specifically to fix things about Node its original
design got locked into early. Deno's permission flags exist because
Node grants a script full filesystem and network access by default,
something a browser tab never would. Bun's headline pitch is raw
speed: it's written in Zig on top of JavaScriptCore instead of V8, and
its package installer resolves and links dependencies dramatically
faster than npm across a large node_modules tree.
Neither has displaced Node as the default choice for most production
backends — the ecosystem of battle-tested libraries and hosting
support still overwhelmingly assumes Node — but both are genuinely
production-ready today, not experiments.
Dry run: picking a runtime for one concrete project
| Question from the table above | Answer for this project | What it rules in or out |
|---|---|---|
| Scenario: a CLI tool that runs untrusted plugin scripts and should start fast, written in TypeScript. | ||
| Needs to sandbox untrusted code? | Yes | rules out Node and Bun — both grant full filesystem/network access by default |
Needs to run .ts directly, no build step? | Yes | rules out Node — needs a transpile step first; Deno and Bun both qualify |
| Depends on a large existing npm/CommonJS ecosystem? | No — few dependencies, all modern | removes the strongest reason to default to Node anyway |
| Conclusion: Deno — the only one of the three that satisfies the sandboxing requirement | ||
Same three rows the table above already gives — this is just running one real scenario through them in order, instead of reading each row in isolation. Change one fact (say, a large existing npm dependency tree the team can't rewrite) and the same walk lands on Node instead.
What a bundler is actually doing: ASTs
Every tool in this chapter — a bundler, a linter, a formatter, a minifier, a codemod — starts the exact same way: parse the source into an Abstract Syntax Tree, a plain nested object describing the code's structure, then walk and transform that tree, never the raw text itself.
// A hand-built AST for: const x = 1 + 2;
// Real parsers (Babel, Acorn) produce something like this automatically.
const ast = {
type: "VariableDeclaration",
kind: "const",
declarations: [{
type: "VariableDeclarator",
id: { type: "Identifier", name: "x" },
init: {
type: "BinaryExpression",
operator: "+",
left: { type: "Literal", value: 1 },
right: { type: "Literal", value: 2 },
},
}],
};
console.log(JSON.stringify(ast.declarations[0].init, null, 2));
That's genuinely close to what astexplorer.net shows for the real thing — every operator, every identifier, every literal is its own typed node. Editing code programmatically (a codemod, the tool behind large automated migrations like a big React version bump across a whole codebase) means finding the right node type in this tree and swapping it, then printing the tree back out to text — never regex-replacing the source directly, which breaks the instant the pattern shows up somewhere the author didn't anticipate (inside a string, a comment, a different context entirely).
A Babel plugin is exactly this walk-and-transform step,
packaged: it's handed the AST, given a chance to visit specific node
types (ArrowFunctionExpression,
ClassDeclaration, …), and returns a modified tree that
Babel then prints back to JS — this is the actual mechanism behind
"transpile modern syntax down to something older browsers run."
Polyfills vs transpilation — two different problems
These get bundled together in conversation constantly, and they fix genuinely different gaps.
| Transpilation | Polyfill | |
|---|---|---|
| Fixes a missing… | syntax — arrow functions, optional chaining, classes | runtime feature — Array.prototype.flat, Promise, fetch |
| How | rewrites your source into older-syntax equivalent code, before shipping | ships extra JS that adds the missing method/object at runtime, if it's not already there |
| Can it be fixed at build time alone? | yes — syntax is fully resolved before the browser ever sees it | no — the feature has to actually exist in the running environment, one way or another |
core-js is the actual polyfill implementation most
tooling pulls from; browser targeting
(a browserslist config, shared by most of this
toolchain) is what tells both the transpiler and the polyfill loader
which engines actually need to be supported — the newer the target
list, the less of either gets shipped, which is a direct, measurable
bundle-size win for a team that can drop support for old browsers.
Reading the spec, and where new syntax comes from
Every JS feature in every chapter on this site started as a TC39 proposal and moved through a fixed series of stages before landing in the language:
| Stage | Means |
|---|---|
| 0 — Strawperson | any committee member's idea, no formal backing yet |
| 1 — Proposal | the problem is real, worth solving, has a champion |
| 2 — Draft | real syntax and semantics written out |
| 2.7 — Approved in principle | the spec text is agreed; it waits for conformance tests and prototype implementations before engines ship it (a stage added in 2024) |
| 3 — Candidate | spec-complete, feedback comes from real implementations, not just discussion |
| 4 — Finished | shipped in engines, included in the next yearly ECMAScript edition |
Optional chaining, nullish coalescing, and top-level await all went through exactly this pipeline before ever reaching a browser. The official spec (ecma262) reads as dense, formal pseudocode — genuinely worth being able to skim once in a while, because it's the actual final authority any time a blog post's explanation of some edge case and the engine's real behavior disagree.
Say it like this → "When I hit a genuinely
ambiguous edge case — something two blog posts explain differently —
I check the spec or a quick
node -p/console test rather
than trust either post. This whole site was built the same way: every
runnable claim in it was verified against a real engine first."
Interview answer, one sentence
"A bundler parses code into an AST to follow imports and drop what is unused, a transpiler rewrites new syntax for older engines, and a polyfill supplies a missing runtime API — three different problems that are constantly confused, and knowing which one you have decides the fix."
Before you move on
- Explain what "structural" typing means for TypeScript, in one sentence.
- Walk the Node/Deno/Bun decision table against a project with different constraints than the dry run above, and land on a different runtime.
- Explain why a bundler or codemod has to work on an AST instead of regex-replacing the source text directly.
- Say the difference between what a transpiler fixes and what a polyfill fixes, and why one can't substitute for the other.
- Name which TC39 stage a proposal needs to reach before it's safe to rely on without a transpiler.
Opens in the editor: write it, run it, and check it against real tests.
All 31 for this level →Finished reading?
0 of 41 chapters read in this series