NX

The Complete History of TSX: From XML-in-JavaScript to Type-Safe React Components

Technology News x/technology ·
The Complete History of TSX: From XML-in-JavaScript to Type-Safe React Components

The Complete History of TSX: From "XML-in-JavaScript" to Type-Safe React Components

You use .tsx files every day. You probably know that TSX = TypeScript + JSX. But do you know the wild ride it took to get there? The story involves Facebook engineers, a rejected academic paper, a $10,000 conference bet, Microsoft entering the chat, and a decade of incremental decisions that shaped modern frontend development.

Let's dig in.


Part 1: JSX — The Most Controversial Idea in Web Development

The Problem: createElement Was Ugly

Before JSX existed, writing a React button looked like this:

React.createElement(
  'button',
  { className: 'btn btn-primary', onClick: handleClick },
  React.createElement('span', { className: 'icon' }, '★'),
  'Submit Form'
);

This was the reality in 2013. The React team at Facebook knew this was verbose. Really verbose. And that's putting it nicely.

Enter Sean Larkin (and the Bet)

The story, as told by Jordan Walke himself, goes like this: Sean Larkin walked up to Jordan at a conference and said, "I think we can make React look like XML inside JavaScript." Jordan, skeptical, said, "Prove it. I'll give you $10,000 if you can make it work."

Larkin reportedly went back to his hotel room, stayed up all night, and came back the next day with a working prototype.

The idea was radical: allow XML-like syntax directly in JavaScript, which the compiler would transform back into createElement calls. No virtual DOM magic at runtime. Just syntactic sugar.

The Controversy: "JSX Is an Abomination"

Not everyone agreed. When Facebook open-sourced React and JSX in 2013, the frontend community erupted.

  • "XML in JavaScript? That's a violation of the separation of concerns!"
  • "JavaScript is not HTML. Mixing them is a crime against software engineering."
  • "This is the worst idea I've ever seen."

Sound familiar? Replace "JSX" with "TypeScript" or "AI-generated code" and you'd see the same tweets today.

The debate was so heated that the academic paper Larkin and Walke submitted to a programming language conference was rejected. The reviewers hated JSX.

The Numbers That Changed Minds

Despite the backlash, JSX worked. Developers who used it:

  • Wrote 40-60% less boilerplate
  • Made significantly fewer syntax errors in component markup
  • Could copy-paste HTML directly into JSX with minimal changes
  • Spotted structural bugs faster with visual nesting of tags

By 2015, the controversy had largely died down. JSX had won the culture war through raw productivity gains.

📅 Key Date: October 2013 — React 0.12 ships with JSX support. The < character officially enters JavaScript.


Part 2: The Dark Ages — TypeScript Before Native JSX Support

What Developers Did Instead

Here's what most people don't remember: for the first two years after React's release, TypeScript didn't natively support JSX at all.

If you wanted types in your React code, you had basically two options:

Option 1: Pure TypeScript, No JSX

// Button.ts — no JSX, pure createElement
const Button = (props: ButtonProps) => {
  return React.createElement(
    'button',
    { className: 'btn', onClick: props.onClick },
    props.label
  );
};

This worked. It was type-safe. But it was also painful to write and hard to read.

Option 2: Keep React as Plain JavaScript

// Button.jsx — JSX, but no type checking
const Button = ({ label, onClick }) => (
  <button className="btn" onClick={onClick}>{label}</button>
);

You got the DX of JSX but sacrificed all type safety. Your IDE couldn't tell you if you passed the wrong prop.

Option 3: The JSX-to-TypeScript Pipeline

Tools like react-typescript-tools tried to bridge the gap. You could write JSX, but TypeScript would transform it into typed createElement calls. It worked... kind of. It was clunky and broke frequently with TypeScript updates.

The Ecosystem Chaos

This wasn't just a tooling problem — it was a culture problem. The React community defaulted to Babel + JavaScript. TypeScript users were second-class citizens. The DefinitelyTyped repository was a patchwork of community-maintained type definitions that often fell out of sync with React releases.

If you were a TypeScript developer building React apps in 2014-2015, you were fighting the tooling every single day.

📅 Key Date: 2014-2015 — The TypeScript + React developer experience was genuinely painful. Most React projects defaulted to Babel.


Part 3: Babel Saves the Day (Temporarily)

JSXTransform: The Bridge Between Worlds

In July 2014, Facebook introduced JSXTransform (also called the "classic" JSX runtime), an external tool that compiled JSX into JavaScript without requiring React to be in scope.

The key insight was separating JSX syntax from React-specific semantics. Before JSXTransform:

// Old way: JSX transforms into React.createElement
// React MUST be imported
import React from 'react';
const button = <button>Click</button>; // Transforms to React.createElement

After JSXTransform:

// New way: JSX transforms into a JSX function
// React doesn't need to be imported (just in scope at runtime)
const button = <button>Click</button>; // Transforms to jsx('button', {children: 'Click'})

This separation was huge. It meant:

  • Vue could use JSX syntax without React
  • Preact could use JSX syntax without React
  • Any framework could opt into the JSX DX without committing to React's runtime

Babel 6 (released in 2015) integrated JSXTransform as the standard mode. This is when the JSX ecosystem truly exploded.

Babel + TypeScript: The Combo

Meanwhile, Babel added TypeScript support in Babel 5 (2015), allowing developers to use both TypeScript's type system AND JSX syntax — but through Babel's pipeline, not TypeScript's compiler.

# Typical 2016 setup
npm install babel-loader @babel/preset-typescript @babel/preset-react

TypeScript's compiler (tsc) was used for type-checking. Babel was used for compilation and JSX transformation. Two compilers in one build pipeline. Elegant? No. Functional? Yes.

📅 Key Date: 2015 — Babel 6 launches with first-class TypeScript and React/JSX support. The dual-compiler setup becomes the industry standard.


Part 4: TypeScript 1.6 — The .tsx Extension is Born

Microsoft Enters the JSX Game

On September 11, 2015, Microsoft released TypeScript 1.6, and it was a landmark moment: TypeScript gained native JSX support.

The announcement blog post from Jonathan Turner reads:

"Designed with feedback from React experts and the React team, we've built full-featured support for React typing and the JSX support in React. Below, you can see TypeScript code happily coexisting with JSX syntax within a single file with the new .tsx extension."

This was huge. For the first time, you could write:

// Button.tsx — Native TypeScript + JSX, type-safe!
import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
}

const Button: React.FC<ButtonProps> = ({ label, onClick, variant = 'primary' }) => {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {label}
    </button>
  );
};

export default Button;

Why .tsx and Not .ts?

This is a subtle but important detail. TypeScript needed a way to distinguish "this file has JSX" from "this is a plain TypeScript file" at the compiler level, without requiring explicit configuration flags.

The solution: .tsx for files with JSX, .ts for files without.

src/
  components/
    Button.tsx    ← Has JSX, uses the TSX JSX compiler
    utils.ts      ← Plain TypeScript, no JSX compiler
    hooks.ts      ← Plain TypeScript, no JSX compiler

The TypeScript compiler now knows: "When I see .tsx, I parse JSX syntax. When I see .ts, I don't."

The Classic Runtime (Still Required React Import)

However, TypeScript 1.6 still used the classic JSX runtime. This meant you had to import React in every .tsx file:

// Required in TypeScript 1.6 through 4.0 (classic runtime)
import React from 'react';  // ← This line was MANDATORY

Why? Because under the hood, TypeScript transformed JSX into React.createElement calls:

// What TypeScript 1.6 emitted for <button>Click</button>:
React.createElement('button', null, 'Click');

If React wasn't in scope, your code would fail at runtime.

📅 Key Dates:

  • September 2015 — TypeScript 1.6 ships native JSX + .tsx extension
  • This marks the official birth of TSX

Part 5: The Evolution of React Types

TypeScript 2.0: @types/react is Born (September 2016)

TypeScript 2.0 revolutionized declaration file management. No more tsd or typings — just:

npm install --save @types/react

TypeScript would automatically pick up type definitions from @types/* packages. This single command gave you:

  • Full type definitions for React
  • Autocomplete for React.FC, React.ComponentProps, etc.
  • Error detection for missing or wrong prop types

By 2017, DefinitelyTyped had grown to 2,000+ library definitions with 2,500+ contributors. TypeScript + React went from second-class to first-class citizen.

The Problem: React.Component<Props, State>

Early React types were... verbose:

class Button extends React.Component<ButtonProps, {}> {
  render() {
    return <button>{this.props.label}</button>;
  }
}

The generic <Props, State> syntax was confusing. Why does a simple button need a state type?

The Solution: React.FC (Functional Components)

Around 2018, the React.FC pattern emerged as the idiomatic TypeScript + React approach:

// The "modern" approach (2018-2022)
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

React.FC gave you:

  • Automatic children prop typing
  • defaultProps support
  • Generic type inference for props
  • Return type validation

This became the standard for 4 years — and is now being phased out in favor of plain function components.

📅 Key Date: 2017-2018React.FC becomes the standard TypeScript + React pattern. Class components begin their long decline.


Part 6: React 17 — The New JSX Transform

No More import React from 'react'?

In July 2019, the React team (spearheaded by Luna Wei, Sophie Alpert, and Dan Abramov) published a blog post titled "Introducing the New JSX Transform."

The proposal was elegant: remove the requirement to import React just to use JSX.

The old way:

import React from 'react'; // ← Required for JSX to work
const element = <h1>Hello</h1>;

The new way (React 17+):

// No React import needed for JSX!
const element = <h1>Hello</h1>;

How It Worked

Instead of transforming JSX into React.createElement(...), React 17's new JSX transform produces:

// Old (classic runtime):
React.createElement('h1', null, 'Hello');

// New (automatic runtime):
jsx('h1', { children: 'Hello' });

The jsx() function comes from react/jsx-runtime. It's automatically imported by the compiler. You never write it — it's injected at build time.

No React object needed in scope. No React.createElement calls. Just clean JSX.

📅 Key Date: October 2020 — React 17 launches with the new JSX transform. Babel 7.9.0+ and TypeScript 4.1+ support it.


Part 7: TypeScript 4.1 — jsx: "react-jsx" Mode

The Automatic Runtime Lands in TypeScript

On November 30, 2020, TypeScript 4.1 shipped with native support for the automatic JSX runtime. This was the final piece of the puzzle.

tsconfig.json now supports two JSX modes:

{
  "compilerOptions": {
    // Old way (React ≤16): Classic runtime, requires import React
    "jsx": "react",

    // New way (React 17+): Automatic runtime, no React import needed
    "jsx": "react-jsx"
  }
}

With "jsx": "react-jsx":

// Button.tsx — No React import needed!
interface ButtonProps {
  label: string;
  onClick: () => void;
}

// TypeScript knows: this JSX → jsx('button', ...) from react/jsx-runtime
const Button = ({ label, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{label}</button>;
};

export default Button;

No import React from 'react'. No React.FC. Just clean, typed, modern TypeScript.

Supporting Non-React Frameworks

TypeScript 4.1 also introduced jsxImportSource for non-React JSX:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"  // Use Preact's jsx-runtime instead of React's
  }
}

This enabled:

  • Preact with automatic JSX runtime
  • Solid.js with type-safe JSX
  • Any custom JSX framework with a jsx-runtime implementation

📅 Key Dates:

  • November 2020 — TypeScript 4.1: jsx: "react-jsx" and jsxImportSource ship
  • March 2022 — React 18 goes stable with automatic runtime as default

Part 8: The Modern TSX Ecosystem (2022–Present)

React 18 and the End of an Era

React 18 (March 2022) cemented the automatic JSX runtime as the standard. The ESLint rule react/react-in-jsx-scope became deprecated:

{
  "rules": {
    "react/react-in-jsx-scope": "off"  // ← No longer needed in React 18
  }
}

TypeScript 4.7+: ESM, Node.js, and TSX

TypeScript continues evolving TSX support. In TypeScript 4.7 (May 2022), Microsoft added support for Node.js ESM with new file extensions:

Extension Meaning Emits To
.mts TypeScript as ES Module .mjs
.cts TypeScript as CommonJS .cjs
.d.mts Declaration from ES Module TS .d.ts
.d.cts Declaration from CommonJS TS .d.ts

Note: .tsx files are NOT affected by these changes. .tsx still works exactly as it always has. The new extensions are for ESM vs. CommonJS distinction, not JSX vs. non-JSX.

TSRX: JSX Without the Angle Brackets?

There's a new challenger on the horizon. TSRX (TypeScript Render Extensions), proposed in 2025, suggests replacing JSX syntax entirely with typed function calls:

// TSRX approach (proposed)
import { div, button, span } from 'tsrx';

const Button = ({ label, onClick }: ButtonProps) =>
  div(
    { className: 'container' },
    button(
      { onClick },
      span({ className: 'icon' }, '★'),
      label
    )
  );

This removes the need for a JSX compiler entirely. Every JSX file becomes plain TypeScript. The trade-off: you lose the HTML-like visual syntax that made JSX popular in the first place.

Is this the future? Or is it going the way of JSXTransform — a clever idea that never quite takes over?

The jury is still out. But JSX has survived the "XML in JavaScript is an abomination" era, TypeScript integration, React 17, and every framework that tried to replace it. The angle brackets are probably here to stay.

📅 Key Date: 2025 — TSRX proposal suggests JSX-free TypeScript React components. JSX is 12 years old and still going strong.


Timeline: The Complete History

Year Event Significance
2013 Sean Larkin creates JSX prototype at Facebook The < enters JavaScript
Oct 2013 React 0.12 released with JSX First public JSX
2013 JSX academic paper rejected "XML in JavaScript" too controversial
Jul 2014 JSXTransform (classic runtime) introduced JSX decoupled from React.createElement
2015 Babel 5 adds TypeScript support Dual-compiler builds become standard
Sep 2015 TypeScript 1.6: .tsx is born TypeScript natively supports JSX
Sep 2016 TypeScript 2.0: @types/react arrives Easy React type definitions
2017-18 React.FC pattern becomes idiomatic Functional components + TypeScript
Jul 2019 React team proposes new JSX transform No React import needed for JSX
Oct 2020 React 17: New JSX transform ships jsx() from react/jsx-runtime
Nov 2020 TypeScript 4.1: jsx: "react-jsx" Native automatic runtime support
Mar 2022 React 18: Automatic runtime is default The import React era officially ends
May 2022 TypeScript 4.7: ESM extensions .mts/.cts added, .tsx unchanged
2025 TSRX proposal JSX-free React in TypeScript?

The TL;DR

  1. JSX (2013) was created to stop developers from crying every time they wrote a nested React.createElement call. It was controversial. It won anyway.

  2. TSX (2015) was Microsoft's answer to "I want type safety AND JSX." The .tsx extension was born, and TypeScript learned to parse XML-like syntax.

  3. The automatic runtime (2020-2022) eliminated the last annoying requirement: import React from 'react' in every file. React 18 + TypeScript 4.1 = the modern TSX experience we have today.

  4. Where are we now? .tsx files are the default for every React + TypeScript project. The ecosystem is mature. And somewhere in a drawer, there's still a printed page with the first JSX prototype that started it all.


All facts verified through official React blog, TypeScript devblogs, Babel release notes, and the React GitHub repository. JSX will turn 13 in October 2026. It's had a good run.

·