What Is a .tsx File?

September 12, 20269 min read
Requirements:
FunctionsGenericsObjects

A .tsx file is a TypeScript file that is allowed to contain JSX. That is the entire definition. Same language, same compiler, same type checker — one extra piece of syntax switched on.

The reason the extension exists at all is a parsing conflict. TypeScript already gave <> a meaning, and JSX wants the same characters for something else. Rather than guess, the compiler looks at the file extension and decides which grammar to use. Everything else in this article follows from that one decision.

What a .tsx file is

Take any React component. The types are ordinary TypeScript, the markup is JSX, and the two live in the same file:

interface GreetingProps {
  name: string
  excited?: boolean
}
 
export function Greeting({ name, excited = false }: GreetingProps) {
  return <p>Hello {name}{excited ? '!' : '.'}</p>
}

Rename that file to Greeting.ts and it stops compiling — not because React is missing, but because <p> is not valid TypeScript outside a .tsx file. The compiler tries to read <p> as a type assertion, runs out of road, and reports a syntax error.

JSX is also not React-specific. Preact, Solid, and Hono all use .tsx files. React is just the most common thing on the other end of it.

.tsx vs .ts vs .jsx vs .js

Four extensions, two independent questions: does this file have types, and does it have markup?

ExtensionTypesJSXUse it for
.tslogic, types, hooks, config, tests
.tsxcomponents and anything that renders
.jsplain JavaScript
.jsxJavaScript components, no type safety

.jsx is the JavaScript sibling of .tsx. Same markup, no type checking. If you are migrating a React codebase to TypeScript, .jsx.tsx is the rename you will be doing a few hundred times.

Test files follow the same rule as everything else. A test that mounts a component and asserts on the rendered output contains JSX, so it belongs in Button.test.tsx. A test for a pure helper function does not, so format.test.ts is correct — and leaving it as .ts keeps the angle-bracket syntax below working.

The practical rule is narrower than most people assume: use .tsx only when the file actually contains JSX. A custom hook, a types.ts module, an API client, a Zod schema — none of those render anything, so none of them need the .tsx grammar. Defaulting everything to .tsx costs you nothing at runtime, but it does turn on the parsing quirks below in files that never needed them.

Nothing ever ships a .tsx file

No browser and no Node runtime understands a .tsx file. The extension is a build-time artefact: something has to read it, throw the types away, turn the JSX into function calls, and write out plain JavaScript. In a Next.js or Vite project that step is already wired up, which is why it feels invisible until the day it breaks.

This matters for one practical reason — types disappear at runtime. Props are checked while you edit and while you build, and then they are gone. If a .tsx file receives data from a network request, the compiler believes whatever you told it, so validate at the boundary rather than trusting the annotation.

The jsx setting in tsconfig

The extension tells the compiler how to parse the file. A separate tsconfig.json option tells it what to emit:

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

The values worth knowing:

If you target something other than React, point jsxImportSource at it:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

Get this wrong and the symptom is confusing: the file parses fine, then the compiler complains that it cannot find react/jsx-runtime, or that your component "cannot be used as a JSX component". The extension and the jsx option are two separate switches, and both have to be on.

Typing a component in a .tsx file

Props are just a parameter type, so an interface or a type alias does the whole job. Both work; interfaces merge and extend more comfortably, which is why most codebases default to them for props.

interface ButtonProps {
  label: string
  variant: 'primary' | 'ghost'
  onClick: () => void
}
 
export function Button({ label, variant, onClick }: ButtonProps) {
  return (
    <button className={variant} onClick={onClick}>
      {label}
    </button>
  )
}

Two things to notice. variant is a union type, so <Button variant="primry" /> is a compile error rather than a silently broken style. And onClick is an ordinary function type — components are functions, and their props are typed with the same tools as everything else.

You will still see React.FC<ButtonProps> in older code:

// ❌ The old habit — extra indirection, and it used to imply an implicit children prop
const Button: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>
 
// ✅ A component is a function. Type the props, let TypeScript infer the return.
function Button({ label }: ButtonProps) {
  return <button>{label}</button>
}

Annotate the props, let the return type be inferred. That is the current recommendation from the React team, and it reads better besides.

For children, React.ReactNode is the type you want — it covers elements, strings, numbers, arrays, null, and undefined:

interface CardProps {
  title: string
  children: React.ReactNode
}
 
export function Card({ title, children }: CardProps) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  )
}

Renaming a .jsx file to .tsx

Migrating a React codebase is mostly this rename, repeated. The order that hurts least:

  1. Turn on allowJs so .js and .jsx files keep working while you convert.
  2. Rename one file, starting with a leaf component that nothing else depends on.
  3. Fix what the compiler reports. On a first rename that is almost always untyped props — every parameter of a component is now an implicit any and strict mode rejects it.
  4. Add a props type, and let inference handle the rest.
// Before — Greeting.jsx, no types, every prop is whatever the caller passed
export function Greeting({ name }) {
  return <p>Hello {name}</p>
}
// After — Greeting.tsx, one interface and the call sites are checked
interface GreetingProps {
  name: string
}
 
export function Greeting({ name }: GreetingProps) {
  return <p>Hello {name}</p>
}

Imports do not change. You import ./Greeting, not ./Greeting.tsx, so renaming a file does not ripple through the files that use it. Resist converting a hundred files in one commit — each rename turns previously invisible bugs into compiler errors, and you want to read them in small batches.

The one real gotcha: generic arrow functions

Here is the parsing conflict biting back. In a .ts file this is a perfectly ordinary generic arrow function:

[object Object]

Move it into a .tsx file and it breaks. The compiler sees <T> and starts parsing a JSX element, then hits the end of the expression looking for a closing tag. The error message — JSX element 'T' has no corresponding closing tag — is at least honest about what happened.

Three fixes, all of them one character or one keyword:

// ✅ A trailing comma tells the parser this is a type parameter list
const identity = <T,>(value: T): T => value
 
// ✅ A constraint does the same job and reads more clearly
const identity2 = <T extends unknown>(value: T): T => value
 
// ✅ Or sidestep it entirely with a function declaration
function identity3<T>(value: T): T {
  return value
}

The trailing comma is the common one, and it looks like a typo forever. If a reviewer keeps deleting it, use the constraint form — same meaning, no mystery.

Angle-bracket type assertions do not work either

Same conflict, second victim. TypeScript's original assertion syntax is unusable in a .tsx file:

declare const input: unknown
 
// ✅ The `as` form works everywhere, which is why it is the one to standardise on
const value = input as string

The <string>input form is a syntax error in any .tsx file, for exactly the reason <T> is. Use as in every file and the question never comes up.

When a .tsx file will not compile

Two failures show up often enough to name.

The first is importing an untyped package. A JavaScript-only component library has no .d.ts files, so TypeScript refuses the import and raises TS7016: Could not find a declaration file for module. Install the matching @types/* package, or write a one-line declaration for it.

The second happens while wiring up tsconfig.json in the first place: if outDir sits inside a directory that include also picks up, the compiler finds its own emitted output on the next run and reports TS5055: Cannot write file, would overwrite input. Move outDir out of the source tree, or exclude it.

Both are configuration problems rather than type problems, which is why they feel so unfair when they land on a file you have not finished writing yet.

Summary

A .tsx file is TypeScript plus JSX, and the extension exists only so the parser knows which meaning of < you intended. Use it for files that render markup and plain .ts for everything else. Set jsx in tsconfig.json to react-jsx unless a bundler owns the transform. And when <T> suddenly stops working, remember which grammar you are in — add the comma and move on.

Share this article

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your what is a .tsx file? knowledge to the test with these related challenges.

#213Vue Basic Props
Hard
#4Pick
Easy
#3312Parameters
Easy

Related Concepts

Concepts that build on or relate to what is a .tsx file?.

InterfacesTypeScript Function TypesTypeScript GenericsUnion Types