27.1 JSX and TSX: The jsx Compiler Option
Right, let’s talk about the jsx compiler option. This is one of those things you set once in your tsconfig.json and then blissfully forget about until your editor starts screaming at you for a reason you don’t understand. It’s not magic, but it is the secret sauce that makes your <div /> turn into something JavaScript can actually run.
The core job of this option is to tell the TypeScript compiler, “Hey, you’re about to see some XML-like syntax that isn’t valid JavaScript. I need you to do one of two things: 1) transform it into regular JavaScript function calls for me, or 2) just leave it alone and let something else (like Babel) handle it.” This decision is crucial because it dictates your entire build pipeline.
The Two Modes: react-jsx and react-jsxdev
This is the modern way. You set this, and TypeScript will transform your lovely JSX into calls to the new jsx runtime functions, which you must import from React (or your chosen library). The biggest win here? You don’t need to import React from 'react' in every single file just to use JSX. It’s a small thing, but it removes a massive source of beginner confusion and linter errors.
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
// ... other options
}
}
With this config, this TSX:
function Welcome({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
Gets compiled to this JavaScript:
import { jsx as _jsx } from "react/jsx-runtime";
function Welcome({ name }) {
return _jsx("h1", { children: `Hello, ${name}` });
}
See? No React.createElement in sight. The react-jsxdev option is the same, but it adds additional debugging information for development builds, which is why tools like Next.js use it automatically.
The Legacy Mode: react
This was the only game in town for years. It transforms JSX into calls to the old faithful React.createElement. The key difference? This method requires that React is in scope. If it’s not, your code throws a runtime error.
// tsconfig.json (Legacy Setup)
{
"compilerOptions": {
"jsx": "react",
// ... other options
}
}
The same Welcome component now compiles to:
function Welcome({ name }) {
return React.createElement("h1", null, "Hello, ", name);
}
This is why, in older codebases and tutorials, you’ll see import React from 'react' even in files that don’t explicitly use anything from it. They’re not importing it for the useState hook; they’re importing it so the compiled JSX doesn’t break. It’s a bit silly, which is why the new runtime is so welcome.
The Hands-Off Modes: preserve and emit
These are for the rare cases where you’re not using React, or you’re handing the JSX off to another tool.
preserve does exactly what it sounds like: it leaves the JSX entirely alone in the output. It just converts the TypeScript to JavaScript and says, “Your problem now, buddy!” This is useful if you’re using a library like Vue that has its own JSX transform, or if you’re using Babel as your primary transpiler and want TypeScript to just handle the type checking.
emit is even more niche. It’s like preserve in that it doesn’t transform the JSX, but it also still emits the output .js files. I’ll be honest, I’ve never had a practical reason to use this. If you’re using Babel, you use preserve. If you’re not, you use one of the react-* options. This one feels like it was added for completeness rather than a common use case.
What This Means For You
So, which one should you use? Unless you’re maintaining a legacy codebase that can’t update its React version, always use "jsx": "react-jsx". It’s cleaner, more performant, and it’s the direction the ecosystem is moving. The only “gotcha” is ensuring your React version is new enough to support it (React 17+).
The most common pitfall is mismatching this setting with your environment. If your tsconfig.json says react-jsx but your project is built with a Webpack config that doesn’t understand the new runtime, you’ll get a jsx-runtime import error. The fix is almost always to update your build tooling or framework—they’ve all supported this for years now. This setting is the key that unlocks the modern JSX transform, and you should absolutely use it.