I'm trying to implement some typings in React that keeps getting an error.
The idea is that I have an enum (EBreakpoint) which is keyed with each device we support. A proxy wrapper component takes each device as a prop, and parses the value as props to the child component.
The TypeScript part works, as I've demonstrated in a Typescript Playground, but the implementation keeps getting this error:
JSX element type 'Element[] | IChild<any>' is not a constructor function for JSX elements.
Type 'Element[]' is missing the following properties from type 'Element': type, props, key
Codesandbox URL: https://codesandbox.io/s/serene-sea-3wmxk (parts of the functionality of proxy is removed, to isolate the issue as much as possible)
index.tsx:
import * as React from "react";
import { render } from "react-dom";
import { Proxy } from "./Proxy";
import "./styles.css";
const ChildElement: React.FC<{ text: string }> = ({ text }) => {
return <>{text}</>;
};
function App() {
return (
<div className="App">
<Proxy Mobile={{ text: "Mobile" }}>
<ChildElement text="Default" />
</Proxy>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Proxy.tsx:
import * as React from "react";
enum EBreakpoint {
Mobile = "Mobile",
Desktop = "Desktop"
}
interface IChild<P> extends React.ReactElement {
props: P;
}
type TResponsiveProps<P> = { [key in EBreakpoint]?: P };
interface IProps<P> extends TResponsiveProps<P> {
children: IChild<P>;
}
export function Proxy<P>({ children, ...breakpoints }: IProps<P>) {
return Object.keys(breakpoints).length && React.isValidElement(children)
? Object.keys(breakpoints).map(breakpoint => (
<div>{React.cloneElement(children, breakpoints[breakpoint])}</div>
))
: children;
}
That's not a bug in your code or in JSX (as suggested in the comments). This is a limitation of JSX in typescript.
With JSX, all your code like
<Proxy Mobile={{ text: "Mobile" }}>
<ChildElement text="Default" />
</Proxy>
will get compiled to
React.createElement(Proxy, {
Mobile: {
text: "Mobile"
}
}, React.createElement(ChildElement, {
text: "Default"
}));
And thus all children will be passed as JSX.Element
to the parent component. This interface is a blackbox and no properties like the prop
types can be retrieved:
The JSX result type
By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.
There is currently an issue open for this, in which a roadmap how JSX.Element
can be generalized to support your use case is described, but without any clear timeline when and to which extent or if it will land.
There is however a quick workaround to still have the functionality you desire, by specifically declaring the generic type for your proxy like this:
Note that this will not perform type checking, to ensure that the two interfaces on Proxy
and on the children match. This is not possible (as described above)
enum EBreakpoint {
Mobile = "Mobile",
Desktop = "Desktop"
}
type TResponsiveProps<P> = { [key in EBreakpoint]?: P };
type IProps<P> = TResponsiveProps<P> & { children?: React.ReactNode };
export function Proxy<P>({
children,
...breakpoints
}: IProps<P>): React.ReactElement | null {
return Object.keys(breakpoints).length && React.isValidElement(children) ? (
<React.Fragment>
{Object.keys(breakpoints).map(breakpoint => (
<div>{React.cloneElement(children, breakpoints[breakpoint])}</div>
))}
</React.Fragment>
) : (
<React.Fragment>{children}</React.Fragment>
);
}
and then in your index.tsx
like this:
<Proxy<{ text: string }> Mobile={{ text: "Mobile" }}>
<ChildElement text="Default" />
</Proxy>
of course you can also extract your prop type { text: string }
into another interface for reuse.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With