Index children of React component with TypeScript - reactjs

I'm making a component that has multiple sets of children.
The question React component with two sets of children suggests to index children, as in props.children[0]. This works great in JavaScript.
But in TypeScript, I'm getting a type error, even though the code works fine at runtime.
function MyComponent(props: { children: React.ReactNode }) {
return <>
...
{props.children[0]}
...
{props.children[1]}
...
</>;
}
TypeScript fails with the following error messages on props.children[0]:
Object is possibly 'null' or 'undefined'.
ts(2533)
Element implicitly has an 'any' type because expression of type '0' can't be used to index type 'string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal'.
Property '0' does not exist on type 'string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal'.
ts(7053)
How do I make it typecheck?

You need to tell Typescript that your component needs multiple children by putting the array brackets:
function MyComponent(props: { children: React.ReactNode[] }) {
// ...
}
If your component accepts exactly 2 children, a tuple type would be preferable so that Typescript will enforce having 2 children:
function MyComponent(props: { children: [React.ReactNode, React.ReactNode] }) {
// ...
}
If your component accepts 2 or more children, you can specify a tuple with a rest element:
function MyComponent(props: { children: [React.ReactNode, ...React.ReactNode[]] }) {
// ...
}

You can use ReactNode[] instead of ReactNode
function MyComponent(props: { children: React.ReactNode[] }) {
return <>
{props.children[0]}
{props.children[1]}
</>;
}
function TestComponent() {
return <MyComponent>
<span>Element 1</span>
<span>Element 2</span>
{'string test'}
{2}
{true}
</MyComponent>
}
Playground

If your component always attempts to render at least 2 child nodes, then you should explicitly type the children parameter that way:
TS Playground
import {type ReactElement, type ReactNode} from 'react';
function MyComponent(props: {
children: [ReactNode, ReactNode, ...readonly ReactNode[]];
}): ReactElement {
return <>
...
{props.children[0]}
...
{props.children[1]}
...
</>;
}
By doing so, TypeScript will emit a compiler error diagnostic if you don't provide at least two children:
function App (): ReactElement {
return (
<MyComponent>{/*
~~~~~~~~~~~
Type '{ children: []; }' is not assignable to type '{ children: [ReactNode, ReactNode, ...ReactNode[]]; }'.
Types of property 'children' are incompatible.
Type '[]' is not assignable to type '[ReactNode, ReactNode, ...ReactNode[]]'.
Source has 0 element(s) but target requires 2.(2322) */}
</MyComponent>
);
}
Similarly, when you only provide one child node:
TS Playground
function App (): ReactElement {
return (
<MyComponent>{/*
~~~~~~~~~~~
This JSX tag's 'children' prop expects type '[ReactNode, ReactNode, ...ReactNode[]]'
which requires multiple children, but only a single child was provided.(2745) */}
<div></div>
</MyComponent>
);
}
But as soon as you provide at least two, the diagnostic disappears:
TS Playground
function App (): ReactElement {
return (
<MyComponent>
<div></div>
<div></div>
</MyComponent>
);
}

Related

Component with function child yields error "Its return type 'ReactNode' is not a valid JSX element."

Consider the following react code: (codesandbox)
import type { ReactNode } from 'react';
type RenderCallbackProps = {
children: (o: { a: number }) => ReactNode,
};
function RenderCallback({ children }: RenderCallbackProps) {
return (children({ a: 123 }));
}
export default function App() {
return (
<RenderCallback>
{({ a }) => (
<h1>{a}</h1>
)}
</RenderCallback>
);
}
This causes the error message:
function RenderCallback({ children }: RenderCallbackProps): ReactNode
'RenderCallback' cannot be used as a JSX component.
Its return type 'ReactNode' is not a valid JSX element.
Type 'undefined' is not assignable to type 'Element | null'.ts(2786)
Why?
Where is the undefined coming from?
How to fix that?
Where is the undefined coming from?
That's part of what ReactNode means. ReactNode is: ReactChild | ReactFragment | ReactPortal | boolean | null | undefined, or basically all of the stuff that you could legally put as the child of a <div>. But while you can put a undefined as a child of a div, it can't be the only thing returned by a component, so you get an error when you try to do return (children({ a: 123 }));
You can fix this one of two ways. If you want to continue to allow null/undefined/boolean etc to be passed in, then just make sure that RenderCallback always wraps it in an element. A fragment will be enough:
function RenderCallback({ children }: RenderCallbackProps) {
return (
<>
{children({ a: 123 })}
</>
)
}
Or if you don't want to allow those values, then you can change the types on the callback. For example only allowing elements:
children: (o: { a: number }) => ReactElement,

Forcing children type in react-typescript: Type 'Element' is not assignable to type 'FunctionComponent<T> [duplicate]

I'm trying to take advantage of the recently added support for typing of children in the TypeScript compiler and #types/react, but struggling. I'm using TypeScript version 2.3.4.
Say I have code like this:
interface TabbedViewProps {children?: Tab[]}
export class TabbedView extends React.Component<TabbedViewProps, undefined> {
render(): JSX.Element {
return <div>TabbedView</div>;
}
}
interface TabProps {name: string}
export class Tab extends React.Component<TabProps, undefined> {
render(): JSX.Element {
return <div>Tab</div>
}
}
When I try to use these components like so:
return <TabbedView>
<Tab name="Creatures">
<div>Creatures!</div>
</Tab>
<Tab name="Combat">
<div>Combat!</div>
</Tab>
</TabbedView>;
I get an error as follows:
ERROR in ./src/typescript/PlayerView.tsx
(27,12): error TS2322: Type '{ children: Element[]; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<TabbedView> & Readonly<{ children?: ReactNode; }> ...'.
Type '{ children: Element[]; }' is not assignable to type 'Readonly<TabbedViewProps>'.
Types of property 'children' are incompatible.
Type 'Element[]' is not assignable to type 'Tab[] | undefined'.
Type 'Element[]' is not assignable to type 'Tab[]'.
Type 'Element' is not assignable to type 'Tab'.
Property 'render' is missing in type 'Element'.
It seems to be inferring the type of children as just Element[] instead of Tab[] even though that's the only type of children I'm using.
EDIT: It would also be fine to restrict the interface of the children props instead of restricting the type of the children components directly, since all I need to do is pull some specific props out of the children components.
Edit 2: Turns out that this approach prevent the warning, but according to the comments TabProps aren't properly checked.
You should try to set children of interface TabbedViewProps like so
interface TabbedViewProps { children?: React.ReactElement<TabProps>[] }
The idea here is not to tell your TabbedView has an array of Tab, but instead tell your TabbedView he has an array of element which takes specific props. In your case TabProps.
Edit ( thx to Matei ):
interface TabbedViewProps {
children?: React.ReactElement<TabProps>[] | React.ReactElement<TabProps>
}
As pointer out already, declaring TabbedView.children as:
children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[];
Will get rid of the error, but it won't be type-checking the children properly. That is, you will still be able to pass children other than TabProps to TabbedView without getting any error, so this would also be valid:
return (
<TabbedView>
<Tab name="Creatures">
<div>Creatures!</div>
</Tab>
<Tab name="Combat">
<div>Combat!</div>
</Tab>
<NotTabButValidToo />
</TabbedView>
);
What you could do instead is declare a prop, let's say tabs: TabProps[], to pass down the props you need to create those Tabs, rather than their JSX, and render them inside TabbedView:
interface TabbedViewProps {
children?: never;
tabs?: TabProps[];
}
...
const TabbedView: React.FC<TabbedViewProps> = ({ tabs }) => {
return (
...
{ tabs.map(tab => <Tab key={ ... } { ...tab } />) }
...
);
};
I tried to assert the type.
You can throw or just ignore.
interface TabbedViewProps {
children?: React.ReactElement<ITabProps> | React.ReactElement<ITabProps>[]
}
And in the component itself map the children and assert or ignore
{React.Children.map(props.children, (tab) => {
if(tab?.type != Tab) return;
console.log(tab?.type == Tab);
return tab;
})}
These answers show the general idea, but they don't allow you to pass children like:
<MyParentComponent>
{condition && <Child1/>}
{list.map((it) => <Child2 x={it}/>}
</MyParentComponent>
I took some inspiration from the definition of children in type PropsWithChildren<P> from the React (v16.14.21) codebase:
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
and came up with a simplified definition that fits my use case:
type TypedReactNode<T> = ReactElement<T> | Array<TypedReactNode<T>> | null | undefined;
type PropsWithTypedChildren<P, C> = P & { children?: TypedReactNode<C> | undefined };
Finally, I can define my component like so:
type MyParentComponentProps = {
whatever: string;
};
const MyParentComponent = (props: PropsWithTypedChildren<MyParentComponentProps, AllowedChildType>) => {
// body
}
Type you are returning in Tab render method is JSX.Element. This is what causes your problem. TabbedView is expecting array of childrens with type Tab. I am not sure if you can specify a certain component as a children type. It can be string or JSX.Element. Can you show the definition file for Tab?
Look at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts to see how JSX.Element interface looks.

Which would be the Typescript Types for React Components that can return a string or directly their children

Which would be the correct types for a React Component that might also return a string or directly their children, apart from a JSX.Element? for example:
type PropsStringExample = Readonly<{
returnString: boolean;
}>;
type PropsChildrenExample = Readonly<{
children: React.ReactNode;
returnChildren: boolean;
}>;
/*
Fails with: Type '{} | null | undefined' is not assignable to type 'ReactElement<any, any> | null'.
Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.
*/
const ComponentReturnsChildren: React.FunctionComponent<PropsChildrenExample> = ({
children,
returnChildren
}: PropsChildrenExample) => {
if (returnChildren) {
return children;
}
return (
<>
<div>Just a wrapper around the children</div>
{children}
</>
);
};
/*
Fails with: Type '"This component returns a string"' is not assignable to type 'ReactElement<any, any> | null'.
*/
const ComponentReturnsString: React.FunctionComponent<PropsStringExample> = ({
returnString
}: PropsStringExample) => {
if (returnString) {
return "This component returns a string";
}
return <div>Or also some dumb div</div>;
};
Wrapping the returned string with a fragment on the last example is not possible either due to some eslint contraints i.e <>"string"</>
CodeSandbox with an example of the errors: https://codesandbox.io/s/type-component-return-children-or-string-b956g?file=/src/index.tsx
A React.FunctionComponent must return a JSX element. A string doesn't match that description and would have to be wrapped in JSX in order to be valid. You could change the type from React.FunctionComponent to JSX.element | string (or something similar), but then in your consuming component you'd have to wrap that in a JSX.element conditionally or use a JSX element that makes sense to contain either a bare string or a JSX.Element. I might suggest that you rethink your approach here, or use a <span> instead of a <> fragment.

What is proper type for React's children as function?

I'm passing a children as a function, returning React.ReactNode into Provider HOC like follows:
<Provider variables={{ id: "qwerty" }}>
{data => (
<ExampleComponent data={data} />
)}
</Provider>
Here's a provider component example code:
type Props = {
id: string;
children: any; // What type should be put here?
};
const Provider: React.SFC<Props> = ({ id, children }) => {
const { data, loading, error } = useQuery<req, reqVariables>(query, {
variables: { id }
});
if (loading) return "Loading...";
if (error) return "Error";
if (!data) return "No data";
return children(data);
}
If I'm not specifying any type for children, so leaving it to default to React.ReactNode, then line return children(data); shows an error:
Cannot invoke an expression whose type lacks a call signature. Type 'string | number | boolean | {} | ReactElement ReactElement Component)> | null) | (new (props: any) => Component)> | ReactNodeArray | ReactPortal' has no compatible call signatures.
If I'm specifying children explicitly in type Props as any, everything works great, but it's Wrong from a Typescript and types perspective.
What is a proper type to be put for props.children, when a function returning React.ReactNode is passed instead of React.ReactNode itself, so Typescript would accept it as a valid "Render function" and will allow calling children like function?
You can specify the children type to be a function which returns a ReactNode
type Props = {
id: string,
children: (props: InjectedCounterProps) => React.ReactNode
};
JSX.Element can also be the type, in case the children being 'DOM element'.

How do I restrict the type of React Children in TypeScript, using the newly added support in TypeScript 2.3?

I'm trying to take advantage of the recently added support for typing of children in the TypeScript compiler and #types/react, but struggling. I'm using TypeScript version 2.3.4.
Say I have code like this:
interface TabbedViewProps {children?: Tab[]}
export class TabbedView extends React.Component<TabbedViewProps, undefined> {
render(): JSX.Element {
return <div>TabbedView</div>;
}
}
interface TabProps {name: string}
export class Tab extends React.Component<TabProps, undefined> {
render(): JSX.Element {
return <div>Tab</div>
}
}
When I try to use these components like so:
return <TabbedView>
<Tab name="Creatures">
<div>Creatures!</div>
</Tab>
<Tab name="Combat">
<div>Combat!</div>
</Tab>
</TabbedView>;
I get an error as follows:
ERROR in ./src/typescript/PlayerView.tsx
(27,12): error TS2322: Type '{ children: Element[]; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<TabbedView> & Readonly<{ children?: ReactNode; }> ...'.
Type '{ children: Element[]; }' is not assignable to type 'Readonly<TabbedViewProps>'.
Types of property 'children' are incompatible.
Type 'Element[]' is not assignable to type 'Tab[] | undefined'.
Type 'Element[]' is not assignable to type 'Tab[]'.
Type 'Element' is not assignable to type 'Tab'.
Property 'render' is missing in type 'Element'.
It seems to be inferring the type of children as just Element[] instead of Tab[] even though that's the only type of children I'm using.
EDIT: It would also be fine to restrict the interface of the children props instead of restricting the type of the children components directly, since all I need to do is pull some specific props out of the children components.
Edit 2: Turns out that this approach prevent the warning, but according to the comments TabProps aren't properly checked.
You should try to set children of interface TabbedViewProps like so
interface TabbedViewProps { children?: React.ReactElement<TabProps>[] }
The idea here is not to tell your TabbedView has an array of Tab, but instead tell your TabbedView he has an array of element which takes specific props. In your case TabProps.
Edit ( thx to Matei ):
interface TabbedViewProps {
children?: React.ReactElement<TabProps>[] | React.ReactElement<TabProps>
}
As pointer out already, declaring TabbedView.children as:
children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[];
Will get rid of the error, but it won't be type-checking the children properly. That is, you will still be able to pass children other than TabProps to TabbedView without getting any error, so this would also be valid:
return (
<TabbedView>
<Tab name="Creatures">
<div>Creatures!</div>
</Tab>
<Tab name="Combat">
<div>Combat!</div>
</Tab>
<NotTabButValidToo />
</TabbedView>
);
What you could do instead is declare a prop, let's say tabs: TabProps[], to pass down the props you need to create those Tabs, rather than their JSX, and render them inside TabbedView:
interface TabbedViewProps {
children?: never;
tabs?: TabProps[];
}
...
const TabbedView: React.FC<TabbedViewProps> = ({ tabs }) => {
return (
...
{ tabs.map(tab => <Tab key={ ... } { ...tab } />) }
...
);
};
I tried to assert the type.
You can throw or just ignore.
interface TabbedViewProps {
children?: React.ReactElement<ITabProps> | React.ReactElement<ITabProps>[]
}
And in the component itself map the children and assert or ignore
{React.Children.map(props.children, (tab) => {
if(tab?.type != Tab) return;
console.log(tab?.type == Tab);
return tab;
})}
These answers show the general idea, but they don't allow you to pass children like:
<MyParentComponent>
{condition && <Child1/>}
{list.map((it) => <Child2 x={it}/>}
</MyParentComponent>
I took some inspiration from the definition of children in type PropsWithChildren<P> from the React (v16.14.21) codebase:
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
and came up with a simplified definition that fits my use case:
type TypedReactNode<T> = ReactElement<T> | Array<TypedReactNode<T>> | null | undefined;
type PropsWithTypedChildren<P, C> = P & { children?: TypedReactNode<C> | undefined };
Finally, I can define my component like so:
type MyParentComponentProps = {
whatever: string;
};
const MyParentComponent = (props: PropsWithTypedChildren<MyParentComponentProps, AllowedChildType>) => {
// body
}
Type you are returning in Tab render method is JSX.Element. This is what causes your problem. TabbedView is expecting array of childrens with type Tab. I am not sure if you can specify a certain component as a children type. It can be string or JSX.Element. Can you show the definition file for Tab?
Look at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts to see how JSX.Element interface looks.

Resources