Capítulo 22 de 36

Chapter 22: JSX

Core Idea

TypeScript understands JSX natively in .tsx files — type-checking both intrinsic elements (lowercase, e.g. <div>) and value-based components (uppercase, e.g. <MyComponent>) against a configurable JSX namespace, with the compiled output shape controlled independently by the jsx compiler option.

Key Concepts

  • Enabling JSX: rename files to .tsx and set the jsx compiler option. Five modes trade off output shape:
    • preserve — keeps JSX as-is for a downstream transform (Babel etc.), emits .jsx.
    • react (classic runtime) — emits React.createElement(...) calls directly, emits .js, no separate transform step needed.
    • react-native — keeps JSX like preserve but emits .js.
    • react-jsx / react-jsxdev — the automatic runtime, emits calls to an auto-imported jsx/jsxDEV helper instead of requiring React in scope.
  • as instead of angle-bracket assertions: <Foo>bar as a type assertion is ambiguous against JSX syntax, so TypeScript disallows angle-bracket assertions entirely in .tsx files — always use bar as Foo there (works identically in .ts files too).
  • Intrinsic vs. value-based elements, distinguished purely by first-letter casing (same convention React uses): lowercase (<div />) is intrinsic, looked up on JSX.IntrinsicElements; uppercase (<MyComponent />) is value-based, resolved as an identifier in scope. If JSX.IntrinsicElements isn't declared at all, intrinsic elements go unchecked entirely; if it is declared, an unlisted tag name is a compile error (a catch-all [elemName: string]: any index signature reopens that).
  • The JSX namespace's location depends on the jsx mode: classic-runtime modes (preserve/react/react-native) look for JSX nested under whatever jsxFactory names (React.JSX for React's default React.createElement factory); automatic-runtime modes (react-jsx/react-jsxdev) instead expect the framework to export a JSX namespace from its jsx-runtime/jsx-dev-runtime entry points. Either way, if the namespace isn't found where expected, both runtimes fall back to a global JSX namespace.
  • Value-based element resolution order: TypeScript first tries resolving a value-based element as a Function Component (a function whose first argument is props and whose return type is assignable to JSX.Element, including via overloads); only if that fails does it try resolving as a Class Component.
  • Class components — element class type vs. element instance type: the element class type is the type of the identifier itself (the class or factory function); the element instance type is what a new (construct signature) or plain call (call signature) on it returns. The instance type must be assignable to JSX.ElementClass (default {} — permissive; a framework can narrow it, e.g. requiring a render method, to reject non-component values used as JSX tags).
  • Attribute (props) type checking: for intrinsic elements, the attribute type is whatever's declared on JSX.IntrinsicElements[tagName]; for value-based elements, it's a specific property of the element instance type, named by JSX.ElementAttributesProperty (defaulting, since TS 2.8, to the constructor's/function's first parameter type if that interface isn't declared). Required/optional props are enforced normally; attribute names that aren't valid JS identifiers (like data-*) are never flagged even if unknown, since they can't correspond to a declared prop name anyway. JSX.IntrinsicAttributes (and the per-class-instance JSX.IntrinsicClassAttributes<T>) model framework-injected extras that aren't real component props — React's key and ref are the canonical examples.
  • Children type checking: children passed between JSX tags maps onto whichever prop name JSX.ElementChildrenAttribute declares (typically children) — its declared type is checked exactly like any other prop, so a component typed to accept a single JSX.Element child will error if given multiple children or mixed text+element children.
  • JSX.Element: the result type of a JSX expression is any unless JSX.Element is declared — even then it's a "black box" type; you can't introspect a JSX expression's own tag/props/children back out of its JSX.Element type.
  • JSX.ElementType (TS 5.1+): overrides what counts as a valid tag to use in JSX (lowercase intrinsic names, function components, class constructors) — independent from what props each one accepts, which is still governed purely by that component's own first parameter type.
  • Embedded expressions ({...} inside JSX) are ordinary TypeScript expressions and type-checked exactly like any other expression — a type error inside a {} block (e.g. dividing a string by a number) is caught the same as outside JSX.

Code Examples

declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number };
  }
}
<foo requiredProp="bar" />;              // OK
<foo />;                                  // Error: requiredProp missing
<foo requiredProp="bar" unknownProp />;   // Error: unknownProp not declared
  • What it demonstrates: intrinsic-element attribute checking driven entirely by the JSX.IntrinsicElements interface — required/optional/unknown props all validated against it.

Reference Tables

jsx modeOutputFile extNotes
preserveJSX kept as-is.jsxfor a downstream transform
reactReact.createElement(...).jsclassic runtime, no extra step
react-nativeJSX kept as-is.js
react-jsx_jsx(...) (auto-imported).jsautomatic runtime
react-jsxdev_jsxDEV(...).jsautomatic dev runtime

Key Takeaways

  1. Always use as T, never <T>value, for type assertions in .tsx files — angle brackets are reserved for JSX syntax there.
  2. Component prop validation always traces back to one interface: JSX.IntrinsicElements[tag] for lowercase tags, or the property named by JSX.ElementAttributesProperty on a component's element instance type for uppercase ones.
  3. JSX.Element is intentionally opaque — don't expect to extract a JSX expression's tag/props back out of its own type.
  4. Match the jsx compiler mode to your actual build pipeline (classic vs. automatic runtime) — mismatches show up as "JSX namespace not found" or unexpected React import requirements.

Connects To

  • React docs skill (if present): React-specific typings for JSX.Element, FC, component prop patterns.
  • Generics: Function Components frequently rely on generic prop typing.
  • Object Types: the interface mechanics (IntrinsicElements, ElementAttributesProperty) all reuse ordinary TypeScript object-type/index-signature rules.