Learn why TypeScript throws TS2786 when a JSX tag is not a valid component type, and how to fix duplicate @types/react, bad returns and wrong imports.
TypeScript error TS2786 means the thing you put between the angle brackets is not usable as a component. The name resolved fine and the props may be perfect — the compiler simply decided that this value cannot produce a JSX element.
The check runs against the JSX types your jsx and jsxImportSource settings point at, which for a React app means @types/react. For a function tag, the compiler asks whether the return type is something React can render. For a class tag, it asks whether the instance type is a React.Component. For anything else — an object, a namespace, a union that includes undefined — it asks whether the value has a call or construct signature at all, and you get TS2604 on the same line as well. TS2786 arrived in TypeScript 3.9 to put a readable headline in front of those older, more cryptic diagnostics.
// The general shape of the error:
// 'OrderSummary' cannot be used as a JSX component.
// Its type '() => { total: number; }' is not a valid JSX element type.
// Type '{ total: number; }' is not assignable to type 'ReactNode | Promise<ReactNode>'.
// ~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// what the tag produces what React can renderAlways read the indented lines under the headline — they name the actual culprit. Its type '() => …' is not a valid JSX element type (or, on older typings, Its return type … is not a valid JSX element) points at what the function returns. Its instance type … is not a valid JSX element points at a class. A typeof import(…) or a union in that position means the tag has no call signature at all. And the notorious Type 'ReactNode' is not assignable to type 'ReactNode' — the same name on both sides — means there are two copies of @types/react in your tree.
@types/react in the Dependency TreeThis is the cause behind most real-world reports, and the one that looks impossible. A component library declares a peer dependency on @types/react@17, your app installs 19, npm hoists one and nests the other, and now ReactNode from copy A is a different type than ReactNode from copy B. Structurally identical, nominally distinct, and the error message helpfully prints both as ReactNode.
// ❌ Broken — the tell-tale "X is not assignable to X"
error TS2786: 'AppProvider' cannot be used as a JSX component.
Its return type 'ReactNode' is not a valid JSX element.
Type 'ReactNode' is not assignable to type 'ReactNode'.
Two different types with this name exist, but they are unrelated.// ✅ Fixed — collapse the tree onto one copy, then reinstall
{
"overrides": {
"@types/react": "19.2.16",
"@types/react-dom": "19.2.16"
}
}overrides is npm; Yarn calls the field resolutions and pnpm uses pnpm.overrides. Delete node_modules and the lockfile entry, reinstall, and confirm with npm ls @types/react that exactly one version is listed. Note that skipLibCheck does not help here: the mismatch surfaces in your JSX, not inside a declaration file.
A function tag has to return a renderable value. If it returns a plain object — a very easy mistake when a return statement is missing its JSX, or when a helper that builds a view model gets used as a component — the compiler rejects the whole tag.
// ❌ Broken
export function OrderSummary() {
return { total: 42 }
}
export const el = <OrderSummary />
// ~~~~~~~~~~~~ Error: 'OrderSummary' cannot be used as a JSX component.
// Its type '() => { total: number; }' is not a valid JSX element type.
// Type '{ total: number; }' is not assignable to type 'ReactNode | Promise<ReactNode>'.// ✅ Fixed — return markup, not data
export function OrderSummary() {
const summary = { total: 42 }
return <p>Total: {summary.total}</p>
}
export const el = <OrderSummary />This cause is strongly version-dependent, and that is worth knowing before you rewrite working code. Before TypeScript 5.1, a JSX tag had to return JSX.Element | null, so returning an array (tags.map(...)), a bare string, undefined, or a Promise also failed with TS2786. TypeScript 5.1 introduced the JSX.ElementType hook, and @types/react 18.2.8 and later use it to accept anything assignable to ReactNode. On a current toolchain all of those return shapes are legal — only genuinely unrenderable values like the object above still error. If you are stuck on older types, wrap multi-element returns in a fragment (return <>{tags.map(...)}</>) and return null instead of undefined.
React.ComponentClass tags are checked on their instance type: the object produced by new must look like a React.Component. A class that only happens to have a render method is not one, so TypeScript lists the members it is missing.
// ❌ Broken
class OrderGrid {
render() {
return null
}
}
export const grid = <OrderGrid />
// ~~~~~~~~~ Error: 'OrderGrid' cannot be used as a JSX component.
// Type 'OrderGrid' is missing the following properties from
// type 'Component<any, any, any>': context, setState, forceUpdate, props, state// ✅ Fixed — extend Component so the instance type matches
import { Component } from "react"
class OrderGrid extends Component<{ rows: number }> {
render() {
return <table>{this.props.rows}</table>
}
}
export const grid = <OrderGrid rows={3} />The same message shows up with third-party class components (the KendoReact Grid reports are the classic example) when the library was compiled against a different @types/react — which puts you back in cause 1, because the base Component it extends is then a different Component than the one your JSX check expects.
If the value has no call or construct signature, TS2604 fires alongside TS2786. The everyday version is a namespace import used as a tag, usually because an editor auto-import guessed wrong.
// ❌ Broken
import * as OrderPanel from "./order-panel"
export const view = <OrderPanel />
// ~~~~~~~~~~ Error: 'OrderPanel' cannot be used as a JSX component.
// Its type 'typeof import("./order-panel")' is not a valid JSX element type.// ✅ Fixed — import the component, not the module namespace
import { OrderPanel } from "./order-panel"
export const view = <OrderPanel />The other version is an optional component prop. Icon?: () => Element is really (() => Element) | undefined, and undefined is not an ElementType, so rendering it unguarded fails even though the happy path is fine.
// ❌ Broken
function ChartIcon() {
return <svg viewBox="0 0 16 16" />
}
function CardHeader({ Icon }: { Icon?: typeof ChartIcon }) {
return (
<header>
<Icon />
{/* Error: 'Icon' cannot be used as a JSX component.
Its type '(() => Element) | undefined' is not a valid JSX element type. */}
<h2>Revenue</h2>
</header>
)
}// ✅ Fixed — narrow the union before rendering it
function CardHeader({ Icon }: { Icon?: typeof ChartIcon }) {
return (
<header>
{Icon && <Icon />}
<h2>Revenue</h2>
</header>
)
}Read the indented line under the error, then count your @types/react copies. If the message says Type 'ReactNode' is not assignable to type 'ReactNode', or names the same type on both sides of anything, stop debugging your component — it is a duplicate-types problem. Run npm ls @types/react (yarn why @types/react, pnpm why @types/react) and look at how many versions come back.
Pin one copy of the React typings with overrides. Add @types/react and @types/react-dom to overrides / resolutions / pnpm.overrides on matching majors, remove node_modules and reinstall. Keep them aligned with the react version you actually ship; a React 19 runtime with React 17 typings will keep producing new variants of this error.
Upgrade TypeScript to 5.1 or later together with @types/react 18.2.8 or later. This is the single change that retires the whole family of "valid return type" complaints — arrays, strings, undefined and the Promise returned by an async Server Component all become legal, because JSX.ElementType lets the typings decide what is renderable.
Make the tag a real component. Return markup rather than data, wrap multi-element returns in a fragment on older types, extend React.Component for class components, and fix the import so you get the named export rather than the module namespace.
Narrow optional components instead of asserting them. {Icon && <Icon />} costs one line and keeps the union honest. Reaching for as any, as React.FC or @ts-expect-error on the tag will silence TS2786 while leaving a real undefined render crash in place — treat those as temporary measures with a comment saying what they are waiting for, never as the fix.
Lock the dependency shape so it cannot drift back. Commit the lockfile, keep the overrides block in place while any dependency still declares an old React peer, and make tsc --noEmit part of CI. Duplicate typings reappear the moment someone adds a library with a stale peer dependency, and this error is the first place it shows.
TS2786 fires when the expression used as a JSX tag is not a valid element type. Concretely: a function whose return type is not renderable, a class whose instance type is not a React.Component, or any value without a call or construct signature — an object, a namespace import, or a union containing undefined. In practice, the most common trigger is not your code at all but two copies of @types/react in the dependency tree, which makes two structurally identical ReactNode types incompatible with each other.
The repeated type name is the signature of a duplicate-typings problem: each ReactNode comes from a different copy of @types/react, so the compiler treats them as unrelated. Run npm ls @types/react — if more than one version appears, force a single one and reinstall:
{
"overrides": {
"@types/react": "19.2.16",
"@types/react-dom": "19.2.16"
}
}Use resolutions for Yarn and pnpm.overrides for pnpm, and pin @types/react-dom to the same major so the two packages agree. skipLibCheck will not hide this one, because the comparison happens in your own JSX.
Because an async function returns Promise<Element>, and before TypeScript 5.1 a component had to return JSX.Element | null. The elaboration reads Its return type 'Promise<Element>' is not a valid JSX element. TypeScript 5.1 added the JSX.ElementType hook and @types/react 18.2.8 adopted it, so a Promise return is now accepted and the error goes away once both are current. On a toolchain you cannot upgrade yet, the documented stopgap was a @ts-expect-error comment on the line above the tag — which is a suppression, not a fix, so remove it as soon as the upgrade lands.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges