JSX and Frameworks

This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

JSX is an expression syntax for element trees. TypeScript parses and type-checks it natively when a file uses the .tsx extension. This page covers only the language side — the compiler options and the JSX.* types — and hands off typing a component’s props, events and refs to the framework pages. The official JSX handbook chapter is the companion reference.

The .tsx extension and the jsx option

A file containing JSX must use .tsx; a plain .ts file rejects angle-bracket elements. The jsx compiler option (see tsconfig.json and Compiler Options) chooses what <div/> compiles to:

  • react-jsx — automatic runtime: calls jsx/jsxs from react/jsx-runtime, no import React. The usual production setting.

  • react-jsxdev — same automatic runtime, development build with extra debugging fields.

  • preserve — leaves JSX in place, emits .jsx for a downstream tool (Babel, esbuild) to transform.

  • react — classic runtime: rewrites elements to React.createElement, so React must be in scope.

  • react-native — like preserve, but emits a .js file.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["ES2022", "DOM"]
  }
}
// greeting.tsx
export function Greeting({ name }: { name: string }) {
  return <p className="greeting">Hello, {name}</p>;
}

as casts are ambiguous in .tsx

The angle-bracket cast form <Foo>value cannot be used in a .tsx file — <Foo> reads as an unclosed JSX tag. Use value as Foo, or prefer value satisfies Foo when you only want the shape checked without widening. When the expression spans several lines, put the as on its own line so it stays readable.

// <string>input;               // Error in .tsx: parsed as a JSX element
const text = input as string;   // ok

const config = {
  retries: 3,
  timeout: 5_000,
} satisfies RequestConfig;      // verifies the shape, keeps the literal types

const root = (
  document.getElementById("app")
) as HTMLDivElement;            // 'as' on its own line for a multi-line expression

JSX.Element vs. React.ReactNode vs. React.ReactElement

  • React.ReactElement — a created element object, roughly \{ type, props, key }; takes type parameters for its props and type, so it is the most precise of the three.

  • JSX.Element — a global alias resolving to ReactElement. Represents a single JSX expression; cannot stand for a string, number, array or null.

  • React.ReactNode — anything React can render: an element, string, number, boolean, null, undefined, or an array of those. The right type for a children prop and for a component’s return value.

  • JSX.IntrinsicElements — global interface mapping built-in tag names to their prop types. Index it to reuse a host element’s props, e.g. JSX.IntrinsicElements['button'].

import type { ReactNode, ReactElement } from "react";

const a: JSX.Element = <span />;                  // exactly one element
const b: ReactElement = <span />;                 // same object, framework-neutral name
const c: ReactNode = ["hi", 42, <span />, null];  // anything renderable

// Reuse the intrinsic <button> props and add one of your own:
type ButtonProps = JSX.IntrinsicElements["button"] & { loading?: boolean };

function Panel({ children }: { children: ReactNode }): ReactNode {
  return <section>{children}</section>;
}

The jsxImportSource pragma and tsconfig option

With jsx: "react-jsx", the automatic runtime is imported from react/jsx-runtime. The jsxImportSource tsconfig option changes that module prefix project-wide, and a per-file /** @jsxImportSource <pkg> */ comment pragma overrides it for one file — useful for Preact, Emotion or solid-js.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
/** @jsxImportSource @emotion/react */
// JSX in this file now calls @emotion/react/jsx-runtime, overriding tsconfig
export const Box = () => <div css={{ padding: 8 }} />;

Typing components — briefly, then defer

Props are just an object type on the first parameter, children is ReactNode, DOM events use React’s synthetic event types, refs use useRef<T>, and a generic component is a generic function (see Generics). Keep annotations minimal and let inference do the rest.

import { useRef, type ChangeEvent, type ReactNode } from "react";

type FieldProps<T extends string> = {
  name: T;
  label: ReactNode;                              // children-style content
  onChange: (name: T, value: string) => void;   // typed callback prop
};

// A generic component is just a generic function:
function Field<T extends string>({ name, label, onChange }: FieldProps<T>) {
  const ref = useRef<HTMLInputElement>(null);   // typed ref
  const handle = (e: ChangeEvent<HTMLInputElement>) => onChange(name, e.target.value);
  return <label>{label}<input ref={ref} onChange={handle} /></label>;
}

This page stops here on purpose. React’s own TypeScript guide is the practical companion; for discriminated props, forwardRef typing, generic components, hooks, context and the React.FC trade-offs see TypeScript with React, and for Angular’s template type-checking, typed @Input()/@Output() and signals see TypeScript Essentials for Angular.

See also