TypeScript · advanced
TypeScript JSX element type error: inspect the component contract
A playbook for diagnosing the TypeScript "JSX element type" error when a value cannot be used as a component under the active type definitions. The guide frames the failure as a contract mismatch between the imported value, the JSX namespace, and tsconfig options such as jsx, jsxImportSource, and module resolution. It sequences triage from the compiler message through declaration lookup, generic constraints, and intrinsic vs component type checks.
The symptoms
- •TypeScript reports an error of the form "TS2786: '<X>' cannot be used as a JSX component (or its return type is not a valid JSX element)" pointing at a JSX usage site.
- •The same identifier compiles cleanly when used as a plain function call (for example, importing and invoking the symbol directly) but fails when used in JSX tag form.
- •Hovering the component in an editor shows a type whose return is not assignable to ReactElement, JSX.Element, or the project's configured JSX.ElementType, often because the return type is inferred as a wider union or a Promise.
- •The error appears only after upgrading React, @types/react, or a component library, or after changing tsconfig "jsx", "jsxImportSource", or "moduleResolution" values.
Likely causes
- •The imported value is not actually a component under the active JSX type definitions; it may be a forwardRef result, a memo return, a context object, a default export of the wrong shape, or a namespace import rather than a callable component.
- •The component's declared or inferred return type is not compatible with the configured JSX element type, often because of generic constraints that widened to a union including non-ReactElement members, or because the return type is Promise or void.
- •Multiple copies of React (or @types/react) are resolved in the project, so the component's React import and the JSX runtime reference different React type packages; the JSX namespace of one does not accept the element type of the other.
- •tsconfig options such as "jsx", "jsxFactory", or "jsxFragmentFactory" are misaligned with the runtime JSX transform, so the compiler is type-checking against a JSX namespace the runtime does not use.
- •The component is declared with an overly permissive generic that the compiler cannot prove returns a valid element, especially when forwardRef or higher-order component helpers are involved.
First ten minutes
- 01Record the full TypeScript error code and message text from the compiler output, including the file path, line, and column of the JSX tag, so subsequent searches against TypeScript release notes remain precise.
- 02Run a project-wide type check (for example, tsc --noEmit) in the failing workspace to confirm the error reproduces outside the editor and to enumerate all occurrences, not just the one currently visible.
- 03Inspect the failing component's declared type by hovering it in the editor or by resolving its declaration file with the TypeScript language server, and compare that type to the JSX.ElementType expected by tsconfig.
- 04Check tsconfig.json values for jsx, jsxImportSource, jsxFactory, and jsxFragmentFactory, and record the moduleResolution mode, because each combination picks a specific JSX namespace.
- 05Verify whether multiple @types/react or react packages are resolved by running a single, read-only dependency listing command and inspecting the deduplicated version per package.
- 06Determine whether the component is a default export, named export, namespace import, or a forwardRef/memo wrapper, because the export shape determines which JSX type check the compiler applies.
Evidence to collect
- •The exact compiler error code (for example, TS2786) and the full diagnostic message text from tsc --noEmit output, since TS2786 has sub-variants that indicate intrinsic vs component failures.
- •The .d.ts file path and line where the failing component's type is declared, obtained via "Go to Definition" or by following the import to the declaration file.
- •The resolved values of tsconfig "jsx", "jsxImportSource", "jsxFactory", "jsxFragmentFactory", and "moduleResolution", read directly from the active tsconfig.json rather than assumed.
- •The deduped version of react and @types/react for the workspace, so duplicate-type mismatches can be ruled in or out as a cause.
- •The shape of the component's exports (default, named, namespace, or wrapper such as forwardRef/memo), because each is checked against the JSX namespace under different rules.
Where to look
- •The boundary between the imported symbol and the JSX tag at the usage site in the consuming file, where the compiler reports the failure and where export shape decisions are visible.
- •The boundary between the component's declaration (.tsx or .d.ts) and its inferred return type, where generic widening, Promise returns, and forwardRef wrappers most often corrupt the JSX element type.
- •The boundary between tsconfig.json (especially "jsx", "jsxImportSource", "moduleResolution") and the package.json of react / @types/react, where version skew between types and runtime JSX transform causes namespace mismatches.
- •The module resolution boundary in the package manager lockfile, where duplicate copies of react or @types/react get installed and produce two distinct React.ElementType definitions.
Diagnostic steps
- 01Read the compiler error code: if it is TS2786 with "cannot be used as a JSX component", the candidate set is non-callable exports, forwardRef/memo return shapes, and React version skew; if it is TS2607 with "JSX element class does not support attributes", the failure is at the class-component boundary.
- 02Resolve the failing symbol to its declaration file and check whether it is a function returning ReactElement, a forwardRef call, a memo call, a class component, or a value re-exported from a namespace; each path uses a different JSX element contract.
- 03Compare the component's return type against the JSX.ElementType implied by tsconfig "jsx" and "jsxImportSource": the classic transform expects React.JSX.Element or React.ReactElement, while the automatic runtime expects React.JSX.ElementType from the configured import source.
- 04Check whether the component's generic parameters cause return-type widening; if so, narrow by adding an explicit constraint that requires the return type to extend the project's element type rather than a permissive unknown or union.
- 05Search the lockfile for duplicated react or @types/react entries and confirm whether the importing package and the JSX runtime share a single resolved version, because two different React type trees break assignability of ElementType.
- 06Verify that the tsconfig "jsx" value matches the runtime expectations: "react-jsx" or "react-jsxdev" should pair with importSource "react" (the default) unless explicitly overridden; mismatches are a common source of TS2786 after upgrades.
- 07Decide whether the fix belongs at the call site (cast or rename), at the component declaration (refine return type or generic constraint), or at the tsconfig boundary (align jsx and jsxImportSource), and choose the narrowest fix that resolves the contract without disabling checks.
Common mistakes
- •Silencing the error with a non-null assertion or an any cast at the JSX tag, which hides the underlying contract mismatch and tends to surface later as a runtime undefined component or wrong-element-type render.
- •Assuming the component is broken when the real issue is duplicate @types/react installations; without resolving the duplicate, every subsequent fix attempt on the component is wasted.
- •Loosening generic constraints to "any" to clear the error, which removes the only thing that would have caught a non-element return type earlier in development.
- •Changing tsconfig "jsx" or "jsxImportSource" to match a guess rather than the actual runtime JSX transform, which can compile but produce runtime "Element type is invalid" errors instead.
- •Confusing a default-export namespace import with a named export; using `import * as Foo from 'foo'` then `<Foo />` fails the JSX component check even when `Foo.default` would succeed.
Safe fixes
- •If the symbol is a namespace import, change the import to a default import or to a named import that resolves to the actual callable component, so the JSX tag sees a component-typed value rather than a module object.
- •If the component is declared as a forwardRef or memo wrapper, ensure the wrapper preserves the component's displayName and that its call signature's return type still extends the project's JSX.ElementType; add an explicit return type annotation if inference widens the result.
- •If generics widen the return type, add an explicit constraint on the generic that requires the produced element type, for example bounding a render-prop generic so its return type extends React.ReactElement, rather than relaxing the constraint to any.
- •If tsconfig "jsx" or "jsxImportSource" does not match the runtime transform, align them with the runtime (for example, "react-jsx" with the default import source) rather than disabling strict checks, and re-run the full project type check.
- •If duplicate react or @types/react are present, deduplicate by aligning the dependency ranges across packages so a single version is resolved, then re-run the type check to confirm the JSX namespace now agrees across the dependency graph.
- •If the component legitimately returns a non-element (for example, a fragment-only render or a portal), annotate the return type explicitly so the compiler can verify it against the JSX contract rather than inferring a broader type.
Prove the fix
- 01Run tsc --noEmit across the project and confirm the original TS2786 (or related) error no longer appears at the previously failing JSX site, and that no new errors were introduced in dependent files.
- 02Inspect the resolved declaration of the component and confirm its return type now extends the configured JSX.ElementType (for example, React.JSX.Element or React.ReactElement depending on tsconfig "jsx"), with no any escape hatch introduced.
- 03Confirm the deduped react and @types/react versions are single-valued per workspace, so the JSX namespace used at the call site and the element type used at the component site are from the same React type tree.
- 04Render the component in a development build (no production deploy required) and verify there is no "Element type is invalid" runtime warning, which would indicate the type fix was illusory.
- 05Re-run the full project type check after any subsequent dependency upgrade to ensure the contract still holds, because JSX element type errors frequently reappear when react or @types/react is bumped.
Prevention and next steps
- •Pin react and @types/react to compatible ranges and avoid mixing classic and automatic JSX transforms in the same tsconfig; align "jsx" and "jsxImportSource" with the runtime transform at the boundary of every workspace.
- •Adopt a single React type tree by deduplicating react and @types/react in the lockfile, and add a CI check that fails the build if more than one version is resolved in the dependency graph.
- •Require explicit return type annotations on shared components, especially those wrapped in forwardRef or memo, so the JSX contract is part of the public type surface rather than inferred permissively.
- •Constrain generics that flow into JSX render positions so the compiler can prove the return type extends the configured element type, rather than widening to a union that may include non-element members.
Safe commands and checks
tsc --noEmit -p tsconfig.json tsc --showConfig -p tsconfig.json tsc --traceResolution -p tsconfig.json 2>&1 | grep -i 'react' npx --yes npm-dedupe --dry-run # review only, do not apply without review ls -1 node_modules/@types/react/package.json node_modules/react/package.json grep -E '"react"|"@types/react"' package.json tsconfig.json