JavaScript Ecosystem Journey: JavaScript is a compiled language?
Exploring how ES6 and TypeScript made JavaScript a compiled language.
Building on the Bundler Foundation
In our previous article, we traced how JavaScript bundlers evolved from manual concatenation tools to sophisticated, invisible systems that handle dependency management and optimization with minimal configuration. But as bundlers became more powerful and easier to use, another dimension of complexity emerged in the JavaScript toolchain: the need to transform JavaScript itself.
While bundlers were solving the “how do we package multiple files” problem, developers were simultaneously facing a new challenge: modern JavaScript features that browsers couldn’t understand, and the desire for type safety in large applications. The solution was compilation. Not in the traditional sense of converting to another language, but transforming JavaScript into… different JavaScript.
The Arrival of Modern JavaScript
In 2015, something big happened. ES6 (later renamed ES2015) arrived with arrow functions, classes, const/let, promises, and modules. Modern JavaScript, finally. Developers looked at these features and thought: “This is what I want to write.”
Then they looked at their browser compatibility matrix and thought: “Oh.”
Internet Explorer 11. Node.js. Safari. None of them supported ES6 properly. And if you wanted to reach actual users, you couldn’t just write ES6 and hope for the best.
The Compilation Problem: Code Transformation
The challenge was fundamental: you needed to transform your source code into something the runtime could actually execute. This transformation could take two forms.
Transpilation: Rewriting for Compatibility
Babel arrived as the solution to backward compatibility. Originally released as 6to5 in September 2014, it did something that felt like magic: it transformed modern ES6+ code into backward-compatible ES5 that everything could run.
// Write this
const add = (a, b) => a + b;
// Babel turns it into this
var add = function(a, b) { return a + b; };
Instant time travel. Write the future, run it in the past. Developers loved it.
But here’s where it got complicated. Babel wasn’t a single tool. It was a platform with plugins and presets. You had to configure it via .babelrc, declaring exactly which JavaScript features you wanted to transform:
{
"presets": ["es2015", "react"],
"plugins": ["transform-class-properties"]
}
And then there were the experimental “stage” features—stage-0, stage-1, stage-2, stage-3—proposals that might never make it into the language. Using them meant risking a future rewrite if the proposal got rejected or changed significantly.
The real nightmare? Integrating Babel with everything else. Babel needed proper configuration to avoid conflicts with your bundler. webpack needed to know how to pipe JavaScript through Babel’s loaders. And the interactions were subtle. Poorly documented. A single misconfigured preset could break your entire build.
Then came the Babel 5 to 6 transition in late 2015. A complete rewrite. Developers had to update plugins, rename packages, rewrite configurations. Massive frustration. But necessary. Because without Babel, you couldn’t write modern JavaScript.
By 2017, Babel had become even more complex with Babel 7’s scoped packages (@babel/core, @babel/preset-env) and more granular plugin management. While this improved maintainability, it meant yet another migration for developers.
Type-Checking as Compilation: Adding Safety Through Analysis
Around the same time, Microsoft released TypeScript in October 2012—a fundamentally different kind of compilation step. While Babel transformed code for compatibility, TypeScript transformed code by stripping away type information that only existed for developer safety.
The pitch was simple: optional static typing. Catch errors at compile time instead of runtime. Better autocomplete. Refactoring confidence.
The community’s reaction? Skeptical. Sometimes hostile.
Purists argued that JavaScript’s dynamic nature was a feature. Others distrusted Microsoft or feared “compile-to-JS languages” as another ecosystem fracture. Developers looked at TypeScript and thought: “Great. Another thing to learn. Another config file.”
And it was. TypeScript meant learning a type system AND configuring tsconfig.json. It meant dealing with the compiler’s opinions about your code. Significant cognitive load.
Then Facebook released Flow in 2014—their own static type checker with a different philosophy and different syntax. Now you had choice paralysis at the type-checking level too. TypeScript or Flow? Neither? Both?
But despite the hostility and complexity, TypeScript quietly provided something valuable: actual safety for massive codebases. Its value would become undeniable by 2019, but in 2015, it just felt like more config overhead.
TypeScript’s genius, however, was in its gradual adoption path. Rather than creating a completely new language or requiring massive rewrites, it offered a superset approach:
// Valid JavaScript - also valid TypeScript
function greet(name) {
console.log("Hello, " + name);
}
// Enhanced TypeScript with optional types
function greet(name: string): void {
console.log("Hello, " + name);
}
// Complex typing for large applications
interface User {
id: number;
name: string;
email?: string;
}
function processUser(user: User): Promise<User> {
return fetch('/api/users/' + user.id).then(response => response.json());
}
All JavaScript was valid TypeScript, and type annotations could be added incrementally. This differed sharply from alternatives that required complete rewrites and breaking changes. You could start with existing JavaScript code, add type annotations where valuable, and get immediate benefits without rewriting everything.
Like Babel, TypeScript required its own build step, but this step performed a different transformation: static analysis followed by type stripping. The compiler would examine your types, catch errors at compile time, and then strip away all type information to produce standard JavaScript:
function greet(person: string): string {
return "Hello, " + person;
}
greet(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
becomes
function greet(person) {
return "Hello, " + person;
}
TypeScript’s breakthrough wasn’t disruption—it was adoption through enhancement. Existing teams could adopt incrementally. Project managers could justify adoption without immediate cost. Developers could learn gradually because TypeScript’s inference system meant many benefits without explicit annotations. And ecosystem integration was seamless since npm packages worked without modification.
This approach was particularly powerful in enterprise environments, where clear interfaces between components, self-enforcing documentation, and safer refactoring provided measurable value.
The Stack Gets Ridiculous
By the end of 2015, here’s what a modern JavaScript project required:
- A task runner (Grunt? Gulp? npm scripts?)
- A bundler (webpack? Browserify? Rollup?)
- A transpiler (Babel, with multiple presets and plugins)
- Optionally, a type checker (TypeScript or Flow)
- And each of these had its own configuration file.
A new project meant:
Gruntfile.jsorgulpfile.jswebpack.config.js.babelrctsconfig.json(maybe)package.json(withnpm scripts)
Not to mention all the subtle interactions between them. Babel presets that needed webpack loaders. Loader chains that needed specific Babel plugins. TypeScript that needed Babel that needed webpack that needed the task runner.
When something broke, the question wasn’t just “where’s the error?” It was “which tool broke? Is it the Babel preset? The webpack loader? The TypeScript compiler? The interaction between all three?”
The Configuration Cascade
Each tool had its own config file, and they often intertwined:
Babel Configuration Evolution
// .babelrc (early versions)
{
"presets": ["es2015", "stage-2", "react"],
"plugins": [
"transform-class-properties",
"transform-object-rest-spread"
]
}
TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"jsx": "react"
}
}
Webpack Integration Complexity
// webpack.config.js snippet showing Babel and TypeScript integration
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
{
loader: 'ts-loader',
options: {
compilerOptions: { target: 'es5' }
}
}
]
}
]
}
};
Competing Solutions and Fragmentation
The JavaScript ecosystem wasn’t just growing taller—it was also splitting into competing approaches:
- Transpilation strategies: Babel with different presets and plugins, each with subtle differences in how they transformed code
- Type-checking systems: TypeScript vs Flow—both could analyze modern JavaScript, but with different philosophies and syntax
- Multiple bundlers: webpack, Browserify, Rollup, Parcel—all with different philosophies
- Task runners: Grunt, Gulp, npm scripts—each with their own configuration patterns
- Module systems: CommonJS, AMD, UMD, ES6 modules—all needed to coexist somehow
This fragmentation meant that developers not only had to choose tools, but also understand how they interacted with each other. Choosing TypeScript didn’t just mean learning types—it meant understanding how TypeScript’s compilation output fed into Babel’s transpilation pipeline, which in turn fed into webpack’s bundling process.
The Pain Was Worth It
Despite the complexity, these compilation tools were solving real problems:
- Browser compatibility: Write modern code, run everywhere
- Code safety: Catch errors before they reach users through static analysis
- Developer experience: Better tooling, autocomplete, and refactoring enabled by type information
- Code organization: Manage complex applications with clear modules and type boundaries
The frustration was valid, but looking back, we can see that without this complexity, we wouldn’t have the sophisticated applications and developer experience we enjoy today.
Peak Chaos: 2016 and the JavaScript Fatigue Crisis
By 2016, the complexity reached a breaking point. Developers were exhausted. The term “JavaScript Fatigue” went viral. Articles with titles like “How it feels to learn JavaScript in 2016” captured the sentiment perfectly: the toolchain had become so overwhelming that newcomers couldn’t tell the difference between essential tools and optional frameworks.
The problem wasn’t just that there were many tools—it was that the toolchain had become a prerequisite to writing any meaningful JavaScript. You couldn’t just write code. You had to understand Babel’s preset system, webpack’s loader chains, possibly TypeScript’s type system, and how they all interacted. And making the wrong choice in any of these decisions could cascade into months of migration work.
The configuration cascade had spiraled into what felt like configuration hell. Every project felt like it required deep expertise in multiple build tools just to get started.
The Stabilization: 2017-2019
After the 2016 peak chaos, the ecosystem gradually began to stabilize:
- Babel and TypeScript started working better together, with TypeScript adopting Babel’s plugin architecture
- webpack became the dominant bundler, establishing clear conventions
- npm scripts largely replaced dedicated task runners
- ES6 modules became the standard, reducing module system fragmentation
- TypeScript’s incremental adoption strategy enabled it to gradually become the default for new JavaScript projects
- Framework conventions (React, Vue, Angular) began to abstract away much of the toolchain complexity
The toolchain had grown from simple script concatenation to a sophisticated compilation pipeline. The difference in maintainability between ES5 and ES6 was huge. But the cost of that maintainability—in configuration, complexity, and cognitive load—had been enormous. This was the price of power, and 2016 was when developers collectively realized that price was very steep.
Modern Perspectives
Looking back from 2025, we can appreciate what this complexity enabled:
- Applications that rival native desktop software in complexity
- Teams that can collaborate safely on massive codebases
- Tooling that provides IDE-quality assistance in editors
- A language that could evolve while maintaining backward compatibility
- Type systems that catch entire classes of bugs before they reach production
JSX: Compiling HTML?
One of the most significant innovations that required compilation was JSX, introduced by React in 2013. Writing HTML-like syntax directly in JavaScript was revolutionary, but browsers couldn’t understand it. This created a new requirement: transforming JSX into standard JavaScript function calls.
// Write this (JSX)
const element = <h1>Hello, world!</h1>;
// Babel transforms it into this (JavaScript)
const element = React.createElement('h1', null, 'Hello, world!');
JSX transformation became one of the most common use cases for Babel, and TypeScript also added support for JSX compilation. This meant that every React developer needed to understand not just the framework, but also the compilation pipeline that made their code work.
The complexity of setting up JSX compilation — configuring Babel presets, webpack loaders, and TypeScript settings — became a barrier to entry for many developers. Yet this barrier was necessary to unlock the power of component-based development that would define the next decade of JavaScript frameworks.
As we’ll see in the next article, this tight coupling between compilation tools and frameworks would drive the industry toward more integrated solutions that abstracted away much of this complexity.
What's Next in This Series
Each article in this series dives deep into one aspect of the JavaScript toolchain evolution, showing not just what changed, but why it changed and what we learned along the way.