TL;DR: This is currently not possible due to the typing of JSX elements in TypeScript. Read on to get some more detail, if you like.
The culprit
From the TS JSX Docs:
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.
The issue pretty much all other issues I found on the topic (like this one) link to is open since February 2018 and still active.
Down the typing rabbit hole
The react typings define JSX.Element
as a React.ReactElement<any, any>
. The fact aside that any, any
doesn't look promising in the first place, let's see if we can make use of ReactElement
in any way to lock down typing any further to help us with typing our children further (we know the answer is no but let's see why that's the case).
Thanks to GitHub's excellent indexing of the React typings, we can easily find the React.ReactElement<P, T>
definition here:
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
telling us that the first type parameter is for the props (so that isn't useful unless we wanted to try and type check based on props, see the alternatives paragraph below). The second parameter sends us down a little further; type: T
sounds interesting and is constrained to string | JSXElementConstructor<any>
. Maybe this gets us further?
Answer is no once more. After finding the definition:
type JSXElementConstructor<P> =
| ((props: P) => ReactElement<any, any> | null)
| (new (props: P) => Component<any, any>);
it turns out that the any
in JSXElementConstructor<any>
also stands for some sort of props.
For completeness sake, looking at the the definition of Component
we will find that it takes the types of the class component's props and state as type arguments.
I tried to work back from there and type Modal's child with something like
type ModalChild = ReactElement<unknown, new (props: unknown) => MHeader>;
function Modal(props: { children: ModalChild | ModalChild[] }) {
const { children } = props
return <>{children}</>
}
however that still somehow allowed spans and divs as children.
There is not much further to go from here so it seems the statement from the JSX docs holds true as well for the specific case of React types.
(Non-)Alternatives
There is a related question on the site which talks about the same issue: How do I restrict the type of React Children in TypeScript, using the newly added support in TypeScript 2.3?. Existing answers suggest typechecking through the props the components take, which however I couldn't get to work (and both high voted answers note that there is no proper typechecking either). I still wanted to mention the idea of this approach for completeness sake.
I am surprised I would ever say this but in this case, Flow has an advantage over TypeScript in that its typings for React (specifically React.Element
) take a generic parameter defining the type of the component, thus allowing only specific items. See their docs page on the topic typing their props as
type Props = {
children: React.ChildrenArray<React.Element<typeof TabBarIOSItem>>,
};
limiting the children of the TabBarIOS
to TabBarIOSItem
s.