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.
createElement Was UglyBefore 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.
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.
Not everyone agreed. When Facebook open-sourced React and JSX in 2013, the frontend community erupted.
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.
Despite the backlash, JSX worked. Developers who used it:
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.
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.
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.
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:
Babel 6 (released in 2015) integrated JSXTransform as the standard mode. This is when the JSX ecosystem truly exploded.
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.
.tsx Extension is BornOn 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
.tsxextension."
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;
.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."
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 +
.tsxextension- This marks the official birth of TSX
@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:
React.FC, React.ComponentProps, etc.By 2017, DefinitelyTyped had grown to 2,000+ library definitions with 2,500+ contributors. TypeScript + React went from second-class to first-class citizen.
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?
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:
children prop typingdefaultProps supportThis became the standard for 4 years — and is now being phased out in favor of plain function components.
📅 Key Date: 2017-2018 —
React.FCbecomes the standard TypeScript + React pattern. Class components begin their long decline.
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>;
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.
jsx: "react-jsx" ModeOn 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.
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:
jsx-runtime implementation📅 Key Dates:
- November 2020 — TypeScript 4.1:
jsx: "react-jsx"andjsxImportSourceship- March 2022 — React 18 goes stable with automatic runtime as default
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 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.
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.
| 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? |
JSX (2013) was created to stop developers from crying every time they wrote a nested React.createElement call. It was controversial. It won anyway.
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.
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.
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.